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
19 changes: 17 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -127,7 +141,8 @@ Media placeholders for this release line. Drop the finished files under docs/ima
</update-image-0.1.0>
-->

[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
[0.1.0]: https://github.com/proofable/sdk/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
7 changes: 5 additions & 2 deletions runtime-mount.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } : {})
},
Expand Down Expand Up @@ -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<string, unknown> | null | undefined} bundle
* @param {string} action
Expand Down Expand Up @@ -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}.`);
}
Expand Down
26 changes: 26 additions & 0 deletions test/runtime-mount.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading