Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,51 @@ All notable changes to `@omadia/dev-platform`. The version that matters is
`packages/plugin/manifest.yaml` — the hub reads the manifest, and `npm run
package` aborts if it disagrees with `packages/plugin/package.json`.

## 0.3.1 — 2026-08-21

Declares the ledger handoff in the manifest so the KERNEL performs it, closing
gap G7 of the 2026-08-21 acceptance run (core issue byte5ai/omadia#814, epic
#470 C15). Requires `@omadia/plugin-api` **1.6.0**; older cores ignore the new
key and keep the previous behaviour.

### Fixed

- **The C11 ledger handoff could never actually seed.** `activate()` calls
`ctx.sql.seedLedger` before `runMigrations`, exactly as C11 documented — but
core runs `permissions.sql.migrations` ITSELF, before `activate()`, so by the
time the plugin got control all nine ledger rows were already written. The
handoff could only ever report `alreadySeeded`, and `skippedNoWitness` — the
one alarm the feature exists to raise — never fired. The acceptance run
measured `0 seeded, 9 already seeded` on the exact upgrade the feature was
built for, and nothing went red: that line is indistinguishable from a healthy
re-run.

This was not fixable from inside the plugin. Core calls the runner before
handing over control, and the witnesses are knowledge only this plugin has.
So the plan is now DECLARED and core executes it first.

### Changed

- **`permissions.sql.handoff: handoff-plan.json`.** The kernel reads the plan
and runs the handoff ahead of its own migration runner. The file is the one
already in the ZIP — the same one `plugin-ledger-handoff.mjs --plan` reads —
so an operator can still dry-run the exact plan against production before
installing anything.
- **The `activate()` `seedLedger` call stays, as the fallback.** On a kernel
that honours `handoff` it reports `alreadySeeded`, which is correct once the
work is done; on an older kernel it remains the only thing that performs the
handoff at all. Removing it would silently drop adoption on every older core.

### Added

- `test/manifest.test.ts` pins the two things the declaration introduces: that
`handoff` names a file the ZIP actually ships (declaring one the ZIP omits
makes core refuse the activation outright, while every local test still
passes), and that the plan satisfies core's STRICTER reader — no unknown keys
(notably `dir`), no duplicate filenames, under the 128 KiB cap, every
filename present in `migrations/`, and a `ledger` that agrees with the
manifest's so core does not warn about a split-brain dry run.

## 0.3.0 — 2026-08-21

Verification pass against omadia core `origin/main` (`9feb3ad3`), the first core
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 30 additions & 1 deletion packages/plugin/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ identity:
# settled on (specs/470-dev-platform-plugin/plan.md §4.1). There is no
# "platform" kind.
kind: "extension"
version: "0.3.0"
version: "0.3.1"
domain: "dev-platform"
description: "The Omadia Dev Platform — dev jobs, runner orchestration, the job pipeline, its own HTTP routes, chat tools and database migrations. Extracted from omadia core into its own installable plugin (epic byte5ai/omadia#470)."
authors:
Expand Down Expand Up @@ -108,6 +108,35 @@ permissions:
migrations: "migrations"
ledger: "plg_omadia_dev_platform_migrations"

# Epic #470 C15 (core issue byte5ai/omadia#814) — the ledger handoff, named
# here so the KERNEL performs it, before it runs `migrations:` above.
#
# C11 gave this plugin `ctx.sql.seedLedger` and documented it as "call this
# before runMigrations". `activate()` does exactly that (src/plugin.ts §3),
# and on a kernel that runs `migrations:` itself it is TOO LATE: the nine
# ledger rows are already written by the time activate() is entered, so the
# handoff can only report `alreadySeeded` and `skippedNoWitness` — the one
# alarm C11 exists to raise — never fires. The 2026-08-21 acceptance run
# measured it: `0 seeded, 9 already seeded` on the exact upgrade the feature
# was built for, indistinguishable in the log from a healthy re-run.
#
# This is not something the plugin can fix by reordering its own code. The
# witnesses are knowledge only this plugin has; the ordering is a decision
# only the kernel can make. So the plan is DECLARED here and the kernel
# executes it first.
#
# The file is the same one `plugin-ledger-handoff.mjs --plan` reads, so an
# operator can dry-run the exact plan against production before installing.
# Its `pluginId` / `ledger` / `migrationsDir` are for that CLI, which has no
# manifest to read them from; the kernel knows all three authoritatively and
# ignores the file's copies (it warns if `ledger` disagrees with the one
# above, because then the table previewed is not the table written).
#
# The `activate()` call STAYS. On a kernel older than this feature it is the
# only thing that performs the handoff; on a kernel that honours this line
# it reports `alreadySeeded`, which is correct — the work is already done.
handoff: "handoff-plan.json"

# Epic #470 C4 / H1 — the prefixes served WITHOUT a kernel session, declared
# here so the operator sees the request at install time and grants it
# explicitly. Each is authenticated, just not by a session:
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@omadia/dev-platform",
"version": "0.3.0",
"version": "0.3.1",
"private": true,
"type": "module",
"main": "dist/plugin.js",
Expand Down
17 changes: 17 additions & 0 deletions packages/plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,23 @@ async function activateInner(
// Guarded: `seedLedger` is optional on the contract, so a core older than
// plugin-api 1.3.0 falls through to the (idempotent) apply loop instead of
// failing to activate.
//
// C15 (core issue byte5ai/omadia#814) — THIS CALL IS NOW THE FALLBACK, not
// the primary path, and it is deliberately kept.
//
// On a core that runs `permissions.sql.migrations` itself before
// `activate()` — which is every core since C7 — this call is structurally
// too late: the ledger rows are already written by the time we get here, so
// it can only report `alreadySeeded` and `skippedNoWitness` never fires.
// The manifest's `permissions.sql.handoff` fixes that by having the kernel
// perform the handoff first, from the same plan file.
//
// Against such a kernel this call is a no-op that reports the work already
// done, which is correct and costs one read-only transaction. Against a
// kernel that does NOT honour `handoff` it is the only thing that performs
// the handoff at all. Removing it would silently drop the adoption on every
// older core; keeping it costs a log line on newer ones. That trade is not
// close.
if (ctx.sql.seedLedger) {
const handoff = await ctx.sql.seedLedger({ entries: SEED_LEDGER_ENTRIES });
log(
Expand Down
82 changes: 82 additions & 0 deletions packages/plugin/test/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const manifest = readFileSync(resolve(pkgRoot, 'manifest.yaml'), 'utf8');
const pkg = JSON.parse(readFileSync(resolve(pkgRoot, 'package.json'), 'utf8')) as {
name: string;
version: string;
files?: string[];
peerDependencies?: Record<string, string>;
};

Expand Down Expand Up @@ -106,6 +107,87 @@ void describe('manifest', () => {
assert.ok((ledger ?? '').length <= 63);
});

void it('permissions.sql.handoff names a plan the ZIP ships', () => {
// Epic #470 C15 (core byte5ai/omadia#814). Declaring this is what makes the
// KERNEL run the handoff before its own migration runner; without it the
// `activate()` call is structurally too late and `skippedNoWitness` never
// fires. A declaration pointing at a file the ZIP does not ship is worse
// than no declaration: the kernel refuses the activation outright.
const handoff = scalar(4, 'handoff');
assert.equal(handoff, 'handoff-plan.json');
assert.ok(
existsSync(resolve(pkgRoot, handoff ?? '')),
'the kernel reads this file at activation and refuses to activate without it',
);
assert.ok(
(pkg.files ?? []).includes(handoff ?? ''),
'declared but not in package.json `files` — present in the repo, absent from the ZIP, ' +
'so every real installation would refuse to activate while every local test passed',
);
});

void it('the declared plan is one the kernel will accept', () => {
// The kernel's reader (`platform/pluginHandoffPlan.ts`) is STRICTER than
// the operator CLI's: it rejects unknown keys, refuses duplicate
// filenames, and caps the file size. A plan this repo is happy with but
// the kernel refuses fails at INSTALL time, on the operator's machine,
// where nobody can fix it. So the kernel's rules are checked here.
const path = resolve(pkgRoot, 'handoff-plan.json');
const raw = readFileSync(path, 'utf8');
assert.ok(
Buffer.byteLength(raw, 'utf8') <= 128 * 1024,
'over the kernel\'s 128 KiB plan cap',
);

const plan = JSON.parse(raw) as Record<string, unknown> & {
entries: { filename: string; witnessSql: string }[];
};

// `pluginId` / `ledger` / `migrationsDir` are the operator CLI's — it runs
// with no manifest and has to be told them. The kernel accepts and ignores
// them so ONE file serves both readers. Everything else is a typo or a key
// from a core this package was not built against, and the kernel refuses
// rather than ignoring it. `dir` is the one that earns the strictness:
// `SeedLedgerOptions` accepts it, so it looks like it should work here.
const allowed = ['pluginId', 'ledger', 'migrationsDir', 'entries', 'dryRun'];
for (const key of Object.keys(plan)) {
assert.ok(allowed.includes(key), `key '${key}' would be refused by the kernel`);
}

assert.ok(Array.isArray(plan.entries) && plan.entries.length > 0);
const seen = new Set<string>();
for (const entry of plan.entries) {
assert.ok(
typeof entry.filename === 'string' && entry.filename.length > 0,
'every entry needs a filename',
);
assert.ok(
typeof entry.witnessSql === 'string' && entry.witnessSql.trim().length > 0,
`entry '${entry.filename}' has no witness — a file with no proof is never seeded`,
);
assert.ok(!seen.has(entry.filename), `'${entry.filename}' is listed twice`);
seen.add(entry.filename);
assert.ok(
existsSync(resolve(pkgRoot, 'migrations', entry.filename)),
`'${entry.filename}' is not in the migrations directory the ZIP ships`,
);
}

// A plan whose ledger disagrees with the manifest makes the kernel warn:
// the operator dry-ran against one table and the kernel writes another.
assert.equal(
plan.ledger,
scalar(4, 'ledger'),
'the plan and the manifest must name the same ledger, or the dry run an operator trusted described a different table',
);
assert.equal(plan.migrationsDir, scalar(4, 'migrations'));
assert.equal(plan.pluginId, pkg.name);

// The kernel takes the directory from the manifest and nowhere else, so a
// plan asking for a dry run would leave the real handoff undone forever.
assert.notEqual(plan.dryRun, true, 'a shipped plan must not ship a dry run');
});

void it('permissions.sql.migrations names the directory the ZIP ships', () => {
assert.equal(scalar(4, 'migrations'), 'migrations');
assert.ok(existsSync(resolve(pkgRoot, 'migrations')));
Expand Down
Loading