From 261a5b426a8ffe7155264ab7da99a25b7faa77c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 18:26:08 +0000 Subject: [PATCH 1/2] Initial plan From be25857030bd83e7748ad72ae8454b45a7938f49 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 18:34:29 +0000 Subject: [PATCH 2/2] feat: implement CapabilityLedger contract and UI/runtime capability reconciliation Agent-Logs-Url: https://github.com/SourceOS-Linux/sourceos-shell/sessions/f703d200-8817-413c-b130-b1ad2c8ef456 Co-authored-by: mdheller <21163552+mdheller@users.noreply.github.com> --- apps/pdf-viewer-demo/index.html | 322 ++++++++++++++++- packages/capability-ledger/package.json | 13 + packages/capability-ledger/src/index.js | 255 ++++++++++++++ packages/capability-ledger/src/schema.js | 62 ++++ .../capability-ledger/tests/ledger.test.js | 324 ++++++++++++++++++ pnpm-lock.yaml | 15 + 6 files changed, 990 insertions(+), 1 deletion(-) create mode 100644 packages/capability-ledger/package.json create mode 100644 packages/capability-ledger/src/index.js create mode 100644 packages/capability-ledger/src/schema.js create mode 100644 packages/capability-ledger/tests/ledger.test.js create mode 100644 pnpm-lock.yaml diff --git a/apps/pdf-viewer-demo/index.html b/apps/pdf-viewer-demo/index.html index fcb80cd..f571740 100644 --- a/apps/pdf-viewer-demo/index.html +++ b/apps/pdf-viewer-demo/index.html @@ -4,9 +4,329 @@ sourceos-shell PDF Viewer Demo +

sourceos-shell PDF Viewer Demo

-

PDF-first runtime scaffold placeholder.

+

PDF-first runtime scaffold — CapabilityLedger surface

+ +

Capability Ledger

+
+ + + + + + + + + + + + + +
CapabilityStateOwnerPolicy / EvidenceConflicts
Initialising ledger…
+
+ +

Feature Gate Demo

+
+

Feature use is blocked until the ledger reports enabled.

+
+ + +
+
+ + +
+
+
+ + diff --git a/packages/capability-ledger/package.json b/packages/capability-ledger/package.json new file mode 100644 index 0000000..468e049 --- /dev/null +++ b/packages/capability-ledger/package.json @@ -0,0 +1,13 @@ +{ + "name": "capability-ledger", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.js", + "./schema": "./src/schema.js" + }, + "scripts": { + "test": "node --test tests/ledger.test.js" + } +} diff --git a/packages/capability-ledger/src/index.js b/packages/capability-ledger/src/index.js new file mode 100644 index 0000000..ff737b5 --- /dev/null +++ b/packages/capability-ledger/src/index.js @@ -0,0 +1,255 @@ +/** + * CapabilityLedger — single source of truth for all capability state. + * + * Each capability flag/state change emits a receipt that records: + * - capabilityId, state, owner, timestamp + * - policyDecisionRef, evidenceRefs, conflictWarnings + * + * Reconciliation occurs at runtime startup, on feature toggle, and on + * plugin load/unload. + * + * Aligned with SourceOS-Linux/sourceos-spec#99. + */ + +import { CAPABILITY_STATES, CAPABILITY_OWNERS } from './schema.js'; + +/** + * @typedef {'declared'|'requested'|'negotiating'|'available'|'enabled'| + * 'degraded'|'blocked_by_policy'|'unsupported_by_runtime'| + * 'unsupported_by_server'|'missing_plugin'|'missing_schema'|'failed'} CapabilityState + */ + +/** + * @typedef {'UI'|'runtime'|'server'|'plugin'|'policy'} CapabilityOwner + */ + +/** + * @typedef {Object} CapabilityReceipt + * @property {string} capabilityId + * @property {CapabilityState} state + * @property {CapabilityOwner} owner + * @property {string} timestamp ISO-8601 + * @property {string|null} policyDecisionRef + * @property {string[]} evidenceRefs + * @property {string[]} conflictWarnings + */ + +/** + * @param {string} capabilityId + * @param {CapabilityState} state + * @param {CapabilityOwner} owner + * @param {Partial>} [opts] + * @returns {CapabilityReceipt} + */ +function buildReceipt(capabilityId, state, owner, opts = {}) { + if (!CAPABILITY_STATES.includes(state)) { + throw new TypeError(`Invalid capability state: "${state}"`); + } + if (!CAPABILITY_OWNERS.includes(owner)) { + throw new TypeError(`Invalid capability owner: "${owner}"`); + } + return { + capabilityId, + state, + owner, + timestamp: new Date().toISOString(), + policyDecisionRef: opts.policyDecisionRef ?? null, + evidenceRefs: opts.evidenceRefs ?? [], + conflictWarnings: opts.conflictWarnings ?? [], + }; +} + +export class CapabilityLedger { + constructor() { + /** @type {Map} */ + this._receipts = new Map(); + } + + // ── internal helper ────────────────────────────────────────────────────── + + /** + * Emit and store a receipt, preserving existing conflictWarnings. + * @param {string} capabilityId + * @param {CapabilityState} state + * @param {CapabilityOwner} owner + * @param {Partial>} [opts] + * @returns {CapabilityReceipt} + */ + _emit(capabilityId, state, owner, opts = {}) { + const existing = this._receipts.get(capabilityId); + const conflictWarnings = [ + ...(existing?.conflictWarnings ?? []), + ...(opts.conflictWarnings ?? []), + ]; + const receipt = buildReceipt(capabilityId, state, owner, { + ...opts, + conflictWarnings, + }); + this._receipts.set(capabilityId, receipt); + return receipt; + } + + // ── state-transition methods ───────────────────────────────────────────── + + /** Move capability to "declared" state. */ + declare(capabilityId, owner, opts = {}) { + return this._emit(capabilityId, 'declared', owner, opts); + } + + /** Move capability to "requested" state. */ + request(capabilityId, owner, opts = {}) { + return this._emit(capabilityId, 'requested', owner, opts); + } + + /** Move capability to "negotiating" state. */ + negotiate(capabilityId, owner, opts = {}) { + return this._emit(capabilityId, 'negotiating', owner, opts); + } + + /** Move capability to "available" state. */ + setAvailable(capabilityId, owner, opts = {}) { + return this._emit(capabilityId, 'available', owner, opts); + } + + /** + * Enable a capability. + * @param {string} capabilityId + * @param {CapabilityOwner} owner + * @param {string|null} policyDecisionRef + * @param {string[]} [evidenceRefs] + */ + enable(capabilityId, owner, policyDecisionRef = null, evidenceRefs = []) { + return this._emit(capabilityId, 'enabled', owner, { policyDecisionRef, evidenceRefs }); + } + + /** + * Deny a capability via policy. + * @param {string} capabilityId + * @param {CapabilityOwner} owner + * @param {string|null} policyDecisionRef + * @param {string[]} [evidenceRefs] + */ + deny(capabilityId, owner, policyDecisionRef = null, evidenceRefs = []) { + return this._emit(capabilityId, 'blocked_by_policy', owner, { policyDecisionRef, evidenceRefs }); + } + + /** Mark capability as degraded. */ + degrade(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'degraded', owner, { evidenceRefs }); + } + + /** Mark capability as unsupported by the runtime. */ + setUnsupportedByRuntime(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'unsupported_by_runtime', owner, { evidenceRefs }); + } + + /** Mark capability as unsupported by the server. */ + setUnsupportedByServer(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'unsupported_by_server', owner, { evidenceRefs }); + } + + /** Mark capability as missing required plugin. */ + setMissingPlugin(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'missing_plugin', owner, { evidenceRefs }); + } + + /** Mark capability as missing required schema. */ + setMissingSchema(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'missing_schema', owner, { evidenceRefs }); + } + + /** Mark capability as failed. */ + fail(capabilityId, owner, evidenceRefs = []) { + return this._emit(capabilityId, 'failed', owner, { evidenceRefs }); + } + + // ── conflict logging ───────────────────────────────────────────────────── + + /** + * Append a conflict warning to an existing receipt without changing state. + * Logs the warning and creates a minimal "declared" receipt if none exists. + * @param {string} capabilityId + * @param {string} warning + */ + logConflict(capabilityId, warning) { + const existing = this._receipts.get(capabilityId); + if (existing) { + existing.conflictWarnings.push(warning); + } else { + // No receipt yet — create one so the warning is attached. + this._emit(capabilityId, 'declared', 'runtime', { conflictWarnings: [warning] }); + } + } + + // ── reconciliation ─────────────────────────────────────────────────────── + + /** + * Reconcile all tracked capabilities. + * + * Any capability not yet in "enabled" state is flagged. Conflicts between + * receipts that claim incompatible states produce logged warnings. + * + * Returns a summary object suitable for runtime startup, feature toggle, + * and plugin load/unload hooks. + * + * @returns {{ enabled: string[], pending: string[], conflicted: string[] }} + */ + reconcile() { + const enabled = []; + const pending = []; + const conflicted = []; + + for (const [capabilityId, receipt] of this._receipts) { + if (receipt.state === 'enabled') { + enabled.push(capabilityId); + } else { + pending.push(capabilityId); + } + if (receipt.conflictWarnings.length > 0) { + conflicted.push(capabilityId); + } + } + + return { enabled, pending, conflicted }; + } + + // ── query methods ──────────────────────────────────────────────────────── + + /** + * Get the current state for a capability. + * Returns `null` if the capability has not been declared. + * @param {string} capabilityId + * @returns {CapabilityState|null} + */ + getState(capabilityId) { + return this._receipts.get(capabilityId)?.state ?? null; + } + + /** + * Get the full receipt for a capability. + * Returns `null` if the capability has not been declared. + * @param {string} capabilityId + * @returns {CapabilityReceipt|null} + */ + getReceipt(capabilityId) { + return this._receipts.get(capabilityId) ?? null; + } + + /** + * Return all receipts as an array. + * @returns {CapabilityReceipt[]} + */ + getAll() { + return Array.from(this._receipts.values()); + } + + /** + * Returns true only when the ledger reports the capability as "enabled". + * Feature use must be gated on this before proceeding. + * @param {string} capabilityId + * @returns {boolean} + */ + isEnabled(capabilityId) { + return this.getState(capabilityId) === 'enabled'; + } +} diff --git a/packages/capability-ledger/src/schema.js b/packages/capability-ledger/src/schema.js new file mode 100644 index 0000000..1bd2e23 --- /dev/null +++ b/packages/capability-ledger/src/schema.js @@ -0,0 +1,62 @@ +/** + * CapabilityLedger receipt JSON schema and valid state/owner enumerations. + * + * Aligned with SourceOS-Linux/sourceos-spec#99. + */ + +export const CAPABILITY_STATES = /** @type {const} */ ([ + 'declared', + 'requested', + 'negotiating', + 'available', + 'enabled', + 'degraded', + 'blocked_by_policy', + 'unsupported_by_runtime', + 'unsupported_by_server', + 'missing_plugin', + 'missing_schema', + 'failed', +]); + +export const CAPABILITY_OWNERS = /** @type {const} */ ([ + 'UI', + 'runtime', + 'server', + 'plugin', + 'policy', +]); + +/** + * JSON Schema (draft-07) for a CapabilityLedger receipt. + */ +export const receiptSchema = { + $schema: 'http://json-schema.org/draft-07/schema#', + $id: 'urn:sourceos:capability-ledger:receipt', + title: 'CapabilityLedgerReceipt', + type: 'object', + required: ['capabilityId', 'state', 'owner', 'timestamp', 'evidenceRefs', 'conflictWarnings'], + additionalProperties: false, + properties: { + capabilityId: { type: 'string', minLength: 1 }, + state: { type: 'string', enum: CAPABILITY_STATES }, + owner: { type: 'string', enum: CAPABILITY_OWNERS }, + timestamp: { type: 'string', format: 'date-time' }, + policyDecisionRef: { type: ['string', 'null'] }, + evidenceRefs: { type: 'array', items: { type: 'string' } }, + conflictWarnings: { type: 'array', items: { type: 'string' } }, + }, +}; + +/** + * Example receipt — used for schema validation and documentation. + */ +export const receiptExample = { + capabilityId: 'pdf-viewer', + state: 'enabled', + owner: 'runtime', + timestamp: '2026-01-01T00:00:00.000Z', + policyDecisionRef: 'policy:allow-pdf-viewer:v1', + evidenceRefs: ['config:features/pdf-viewer:enabled', 'plugin:pdf-renderer:loaded'], + conflictWarnings: [], +}; diff --git a/packages/capability-ledger/tests/ledger.test.js b/packages/capability-ledger/tests/ledger.test.js new file mode 100644 index 0000000..944e469 --- /dev/null +++ b/packages/capability-ledger/tests/ledger.test.js @@ -0,0 +1,324 @@ +/** + * CapabilityLedger runtime tests. + * + * Covers acceptance criteria from SourceOS-Linux/sourceos-spec#99: + * - enabling a capability + * - denying (blocking by policy) + * - unsupported by runtime / server + * - missing plugin + * - failed reconciliation + * - conflict warnings + * - receipt schema conformance + */ + +import { test, describe } from 'node:test'; +import assert from 'node:assert/strict'; + +import { CapabilityLedger } from '../src/index.js'; +import { CAPABILITY_STATES, CAPABILITY_OWNERS, receiptSchema, receiptExample } from '../src/schema.js'; + +// ── schema sanity ──────────────────────────────────────────────────────────── + +describe('receiptSchema', () => { + test('contains all required capability states', () => { + assert.deepEqual(receiptSchema.properties.state.enum, CAPABILITY_STATES); + }); + + test('contains all required capability owners', () => { + assert.deepEqual(receiptSchema.properties.owner.enum, CAPABILITY_OWNERS); + }); + + test('example receipt matches schema required fields', () => { + for (const field of receiptSchema.required) { + assert.ok(field in receiptExample, `receiptExample missing required field: ${field}`); + } + }); + + test('example receipt state is a valid state', () => { + assert.ok( + CAPABILITY_STATES.includes(receiptExample.state), + `example state "${receiptExample.state}" not in CAPABILITY_STATES`, + ); + }); + + test('example receipt owner is a valid owner', () => { + assert.ok( + CAPABILITY_OWNERS.includes(receiptExample.owner), + `example owner "${receiptExample.owner}" not in CAPABILITY_OWNERS`, + ); + }); +}); + +// ── enabling ───────────────────────────────────────────────────────────────── + +describe('enabling a capability', () => { + test('state transitions: declared → enabled', () => { + const ledger = new CapabilityLedger(); + ledger.declare('pdf-viewer', 'runtime'); + assert.equal(ledger.getState('pdf-viewer'), 'declared'); + + ledger.enable('pdf-viewer', 'runtime', 'policy:allow-pdf:v1', ['config:pdf:on']); + assert.equal(ledger.getState('pdf-viewer'), 'enabled'); + assert.equal(ledger.isEnabled('pdf-viewer'), true); + }); + + test('receipt has correct fields after enable', () => { + const ledger = new CapabilityLedger(); + ledger.enable('pdf-viewer', 'runtime', 'policy:allow-pdf:v1', ['config:pdf:on']); + const receipt = ledger.getReceipt('pdf-viewer'); + + assert.equal(receipt.capabilityId, 'pdf-viewer'); + assert.equal(receipt.state, 'enabled'); + assert.equal(receipt.owner, 'runtime'); + assert.equal(receipt.policyDecisionRef, 'policy:allow-pdf:v1'); + assert.deepEqual(receipt.evidenceRefs, ['config:pdf:on']); + assert.ok(typeof receipt.timestamp === 'string'); + }); + + test('isEnabled returns false before enable', () => { + const ledger = new CapabilityLedger(); + ledger.declare('feature-x', 'UI'); + assert.equal(ledger.isEnabled('feature-x'), false); + }); + + test('isEnabled returns false for unknown capability', () => { + const ledger = new CapabilityLedger(); + assert.equal(ledger.isEnabled('unknown'), false); + }); +}); + +// ── denying ────────────────────────────────────────────────────────────────── + +describe('denying a capability via policy', () => { + test('state is blocked_by_policy after deny', () => { + const ledger = new CapabilityLedger(); + ledger.declare('restricted-feature', 'policy'); + ledger.deny('restricted-feature', 'policy', 'policy:deny-restricted:v2', ['audit:policy-log:42']); + + assert.equal(ledger.getState('restricted-feature'), 'blocked_by_policy'); + assert.equal(ledger.isEnabled('restricted-feature'), false); + }); + + test('receipt records policyDecisionRef and evidenceRefs', () => { + const ledger = new CapabilityLedger(); + ledger.deny('feature-y', 'policy', 'policy:deny-feature-y:v1', ['log:entry:99']); + const receipt = ledger.getReceipt('feature-y'); + + assert.equal(receipt.policyDecisionRef, 'policy:deny-feature-y:v1'); + assert.deepEqual(receipt.evidenceRefs, ['log:entry:99']); + }); +}); + +// ── unsupported by runtime ─────────────────────────────────────────────────── + +describe('unsupported by runtime', () => { + test('state is unsupported_by_runtime', () => { + const ledger = new CapabilityLedger(); + ledger.setUnsupportedByRuntime('webgpu', 'runtime', ['runtime:version:1.0.0']); + + assert.equal(ledger.getState('webgpu'), 'unsupported_by_runtime'); + assert.equal(ledger.isEnabled('webgpu'), false); + }); + + test('evidenceRefs are recorded', () => { + const ledger = new CapabilityLedger(); + ledger.setUnsupportedByRuntime('webgpu', 'runtime', ['runtime:caps:no-webgpu']); + assert.deepEqual(ledger.getReceipt('webgpu').evidenceRefs, ['runtime:caps:no-webgpu']); + }); +}); + +// ── unsupported by server ──────────────────────────────────────────────────── + +describe('unsupported by server', () => { + test('state is unsupported_by_server', () => { + const ledger = new CapabilityLedger(); + ledger.setUnsupportedByServer('live-collab', 'server', ['server:version:0.9']); + + assert.equal(ledger.getState('live-collab'), 'unsupported_by_server'); + assert.equal(ledger.isEnabled('live-collab'), false); + }); +}); + +// ── missing plugin ─────────────────────────────────────────────────────────── + +describe('missing plugin', () => { + test('state is missing_plugin', () => { + const ledger = new CapabilityLedger(); + ledger.setMissingPlugin('ink-sign', 'plugin', ['plugin:ink-renderer:not-installed']); + + assert.equal(ledger.getState('ink-sign'), 'missing_plugin'); + assert.equal(ledger.isEnabled('ink-sign'), false); + }); + + test('receipt records evidenceRefs for missing plugin', () => { + const ledger = new CapabilityLedger(); + ledger.setMissingPlugin('ink-sign', 'plugin', ['plugin:ink-renderer:not-installed']); + const receipt = ledger.getReceipt('ink-sign'); + assert.deepEqual(receipt.evidenceRefs, ['plugin:ink-renderer:not-installed']); + }); +}); + +// ── missing schema ─────────────────────────────────────────────────────────── + +describe('missing schema', () => { + test('state is missing_schema', () => { + const ledger = new CapabilityLedger(); + ledger.setMissingSchema('doc-export', 'runtime', ['schema:export-v2:not-found']); + + assert.equal(ledger.getState('doc-export'), 'missing_schema'); + assert.equal(ledger.isEnabled('doc-export'), false); + }); +}); + +// ── failed reconciliation ──────────────────────────────────────────────────── + +describe('failed reconciliation', () => { + test('state is failed', () => { + const ledger = new CapabilityLedger(); + ledger.fail('pdf-sign', 'runtime', ['error:load:timeout']); + + assert.equal(ledger.getState('pdf-sign'), 'failed'); + assert.equal(ledger.isEnabled('pdf-sign'), false); + }); + + test('reconcile reports failed capability as pending', () => { + const ledger = new CapabilityLedger(); + ledger.enable('cap-a', 'runtime'); + ledger.fail('cap-b', 'runtime', []); + + const { enabled, pending } = ledger.reconcile(); + assert.ok(enabled.includes('cap-a')); + assert.ok(pending.includes('cap-b')); + }); + + test('reconcile returns all enabled capabilities', () => { + const ledger = new CapabilityLedger(); + ledger.enable('cap-a', 'runtime'); + ledger.enable('cap-b', 'runtime'); + + const { enabled, pending } = ledger.reconcile(); + assert.deepEqual(enabled.sort(), ['cap-a', 'cap-b']); + assert.deepEqual(pending, []); + }); +}); + +// ── conflict warnings ──────────────────────────────────────────────────────── + +describe('conflict warnings', () => { + test('logConflict appends warning to existing receipt', () => { + const ledger = new CapabilityLedger(); + ledger.declare('cap-conflict', 'runtime'); + ledger.logConflict('cap-conflict', 'UI claims enabled but runtime reports unsupported'); + + const receipt = ledger.getReceipt('cap-conflict'); + assert.equal(receipt.conflictWarnings.length, 1); + assert.equal(receipt.conflictWarnings[0], 'UI claims enabled but runtime reports unsupported'); + }); + + test('logConflict creates a receipt for an unknown capability', () => { + const ledger = new CapabilityLedger(); + ledger.logConflict('ghost-cap', 'claimed by UI but never declared'); + + const receipt = ledger.getReceipt('ghost-cap'); + assert.ok(receipt); + assert.equal(receipt.conflictWarnings.length, 1); + }); + + test('conflicting emit preserves prior conflict warnings', () => { + const ledger = new CapabilityLedger(); + ledger.declare('cap-x', 'runtime'); + ledger.logConflict('cap-x', 'warning 1'); + ledger.enable('cap-x', 'runtime'); + + const receipt = ledger.getReceipt('cap-x'); + assert.equal(receipt.state, 'enabled'); + assert.ok(receipt.conflictWarnings.includes('warning 1')); + }); + + test('reconcile includes conflicted capabilities', () => { + const ledger = new CapabilityLedger(); + ledger.enable('cap-conflict', 'runtime'); + ledger.logConflict('cap-conflict', 'server disagrees'); + + const { conflicted } = ledger.reconcile(); + assert.ok(conflicted.includes('cap-conflict')); + }); +}); + +// ── getAll ─────────────────────────────────────────────────────────────────── + +describe('getAll', () => { + test('returns all tracked receipts', () => { + const ledger = new CapabilityLedger(); + ledger.enable('a', 'runtime'); + ledger.deny('b', 'policy', null, []); + + const all = ledger.getAll(); + assert.equal(all.length, 2); + assert.ok(all.find(r => r.capabilityId === 'a')); + assert.ok(all.find(r => r.capabilityId === 'b')); + }); +}); + +// ── receipt timestamp ──────────────────────────────────────────────────────── + +describe('receipt timestamp', () => { + test('timestamp is a valid ISO-8601 date string', () => { + const ledger = new CapabilityLedger(); + ledger.enable('ts-test', 'runtime'); + const receipt = ledger.getReceipt('ts-test'); + const parsed = new Date(receipt.timestamp); + assert.ok(!isNaN(parsed.getTime()), 'timestamp is not a valid date'); + }); +}); + +// ── invalid inputs ─────────────────────────────────────────────────────────── + +describe('invalid inputs', () => { + test('throws on invalid state', () => { + const ledger = new CapabilityLedger(); + assert.throws( + () => ledger._emit('cap', 'invalid_state', 'runtime'), + /Invalid capability state/, + ); + }); + + test('throws on invalid owner', () => { + const ledger = new CapabilityLedger(); + assert.throws( + () => ledger._emit('cap', 'enabled', 'unknown-owner'), + /Invalid capability owner/, + ); + }); +}); + +// ── full lifecycle ─────────────────────────────────────────────────────────── + +describe('full lifecycle', () => { + test('declared → requested → negotiating → available → enabled', () => { + const ledger = new CapabilityLedger(); + const id = 'feature-lifecycle'; + + ledger.declare(id, 'UI'); + assert.equal(ledger.getState(id), 'declared'); + + ledger.request(id, 'runtime'); + assert.equal(ledger.getState(id), 'requested'); + + ledger.negotiate(id, 'server'); + assert.equal(ledger.getState(id), 'negotiating'); + + ledger.setAvailable(id, 'runtime'); + assert.equal(ledger.getState(id), 'available'); + + ledger.enable(id, 'policy', 'policy:allow:v1', ['evidence:1']); + assert.equal(ledger.getState(id), 'enabled'); + assert.equal(ledger.isEnabled(id), true); + }); + + test('degraded state does not count as enabled', () => { + const ledger = new CapabilityLedger(); + ledger.degrade('degrade-test', 'runtime', ['perf:low']); + assert.equal(ledger.isEnabled('degrade-test'), false); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..80b5c2b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,15 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + packages/capability-ledger: {} + + services/docd: {} + + services/pdf-secure: {}