diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f18ef8..a102305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Product release notes: [docs.proofable.me/changelog](https://docs.proofable.me/changelog). +## [Unreleased] + +## [0.1.4] - 2026-10-05 + +### Fixed + +- **Delegated mounts fail closed without an allowlist.** `evaluateRuntimeAction` now denies a delegated action when the mounted permission proof has an empty or missing `allowedActions` list, and `buildRuntimeBundle` preserves a present-but-empty list instead of dropping it. Controller-owned mounts (no delegation) keep their existing behavior. + +### Upgrade + +```bash +npm install @proofable/sdk@0.1.4 +``` + ## [0.1.2] - 2026-09-24 ### Changed @@ -127,7 +141,8 @@ Media placeholders for this release line. Drop the finished files under docs/ima --> -[Unreleased]: https://github.com/proofable/sdk/compare/v0.1.2...HEAD +[Unreleased]: https://github.com/proofable/sdk/compare/v0.1.4...HEAD +[0.1.4]: https://github.com/proofable/sdk/compare/v0.1.3...v0.1.4 [0.1.2]: https://github.com/proofable/sdk/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/proofable/sdk/compare/v0.1.0...v0.1.1 -[0.1.0]: https://github.com/proofable/sdk/releases/tag/v0.1.0 \ No newline at end of file +[0.1.0]: https://github.com/proofable/sdk/releases/tag/v0.1.0 diff --git a/package.json b/package.json index 242710e..6bdc9b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@proofable/sdk", - "version": "0.1.3", + "version": "0.1.4", "description": "SDK and CLI for verification gates, reusable proof, and agent permissions.", "bin": { "proofable": "cli/proofable.mjs" diff --git a/runtime-mount.js b/runtime-mount.js index 071a003..acbf820 100644 --- a/runtime-mount.js +++ b/runtime-mount.js @@ -310,7 +310,7 @@ export function buildRuntimeBundle(input) { memoryRefs: Array.isArray(input.memoryRefs) ? input.memoryRefs : undefined, enforce: { deniedActions, - ...(allowedActions?.length ? { allowedActions } : {}), + ...(delegation ? { allowedActions } : {}), ...(requiresHumanApproval ? { requiresHumanApproval: true } : {}), ...(approvalPolicy ? { approvalPolicy } : {}) }, @@ -586,7 +586,7 @@ export function evaluateMountFileHealth(manifest) { /** * Evaluate one host action against the current runtime-mount permission bundle. * Denies when identity or permission state is missing or expired. A denied action - * always wins; a non-empty allowlist denies actions it does not include. + * always wins; delegated authority requires an explicit, non-empty allowlist. * * @param {import('./runtime-mount.js').RuntimeMountBundle | Record | null | undefined} bundle * @param {string} action @@ -623,6 +623,9 @@ export function evaluateRuntimeAction(bundle, action, options = {}) { } const allowedActions = asStringArray(bundle.enforce?.allowedActions).map(value => value.toLowerCase()); + if (bundle.authority?.mode === 'delegated' && allowedActions.length === 0) { + return deny('ACTION_NOT_ALLOWED', `The current permission proof does not allow ${normalizedAction}.`); + } if (allowedActions.length > 0 && !allowedActions.includes(normalizedAction)) { return deny('ACTION_NOT_ALLOWED', `The current permission proof does not allow ${normalizedAction}.`); } diff --git a/test/runtime-mount.test.js b/test/runtime-mount.test.js index 00af890..706f1d3 100644 --- a/test/runtime-mount.test.js +++ b/test/runtime-mount.test.js @@ -405,6 +405,32 @@ describe('runtime-mount', () => { }); }); + it('grants no delegated actions when the allowlist is empty or absent', () => { + for (const allowedActions of [[], undefined]) { + const bundle = buildRuntimeBundle({ + identity, + delegation: { ...delegation, allowedActions, deniedActions: [] } + }); + + expect(bundle.enforce.allowedActions).toEqual([]); + expect(evaluateRuntimeAction(bundle, 'send_message')).toMatchObject({ + decision: 'denied', + allowed: false, + code: 'ACTION_NOT_ALLOWED' + }); + } + }); + + it('preserves controller authority without a delegation allowlist', () => { + const bundle = buildRuntimeBundle({ identity, controllerWallet: identity.agentWallet }); + + expect(evaluateRuntimeAction(bundle, 'read_proofs')).toMatchObject({ + decision: 'allowed', + allowed: true, + code: 'ACTION_ALLOWED' + }); + }); + it('fails closed when permission state is missing or expired', () => { const identityOnly = buildRuntimeBundle({ identity }); const expired = buildRuntimeBundle({