-
-
Notifications
You must be signed in to change notification settings - Fork 30
feat(security): provenance-keyed sandbox tiers for the app capability model #1574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
113439b
feat(sandbox): add provenance tiers and a capability ceiling model
jaylfc ebe93df
feat(sandbox): enforce provenance ceilings in the broker and permissi…
jaylfc 0257fe2
feat(sandbox): thread provenance into SandboxedAppWindow
jaylfc 07de06b
feat(sandbox): show a provenance badge in the App Studio publish view
jaylfc 1a29bcb
refactor(sandbox): fold Kilo review notes (namespace match, migration…
jaylfc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { describe, it, expect } from "vitest"; | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { PublishView } from "../PublishView"; | ||
|
|
||
| describe("PublishView -- provenance badge", () => { | ||
| it("shows an AI-generated provenance badge next to the app name", () => { | ||
| render(<PublishView />); | ||
| const badge = screen.getByTestId("provenance-badge"); | ||
| expect(badge.getAttribute("data-provenance")).toBe("ai-generated"); | ||
| expect(badge.textContent).toContain("AI-generated"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| """The provenance -> capability ceiling model (provenance-keyed sandbox tiers). | ||
|
|
||
| Every userspace app is classified into one of four provenance tiers | ||
| (first-party/ai-generated/user-uploaded/unknown); the tier is a CEILING -- | ||
| the capabilities the app holds automatically, before any user consent. | ||
| """ | ||
| import pytest | ||
|
|
||
| from tinyagentos.userspace.capabilities import ( | ||
| DEFAULT_PROVENANCE, | ||
| FREE_CAPS, | ||
| GATED_CAPS, | ||
| KNOWN_CAPS, | ||
| PROVENANCE_CEILINGS, | ||
| PROVENANCE_DESCRIPTIONS, | ||
| PROVENANCE_TIERS, | ||
| capability_allowed, | ||
| capability_ceiling, | ||
| default_provenance_for_trust, | ||
| is_known_provenance, | ||
| ) | ||
|
|
||
|
|
||
| def test_provenance_tiers_are_exactly_four(): | ||
| assert PROVENANCE_TIERS == ("first-party", "ai-generated", "user-uploaded", "unknown") | ||
|
|
||
|
|
||
| def test_every_tier_has_a_ceiling_and_description(): | ||
| for tier in PROVENANCE_TIERS: | ||
| assert tier in PROVENANCE_CEILINGS | ||
| assert PROVENANCE_DESCRIPTIONS.get(tier), f"no description for {tier}" | ||
|
|
||
|
|
||
| def test_default_provenance_is_unknown_the_most_restricted(): | ||
| assert DEFAULT_PROVENANCE == "unknown" | ||
| assert capability_ceiling(DEFAULT_PROVENANCE) == frozenset() | ||
|
|
||
|
|
||
| def test_is_known_provenance(): | ||
| for tier in PROVENANCE_TIERS: | ||
| assert is_known_provenance(tier) is True | ||
| assert is_known_provenance("community") is False | ||
| assert is_known_provenance("") is False | ||
| assert is_known_provenance(None) is False | ||
|
|
||
|
|
||
| def test_first_party_ceiling_is_every_known_capability(): | ||
| assert capability_ceiling("first-party") == KNOWN_CAPS | ||
|
|
||
|
|
||
| def test_ai_generated_and_user_uploaded_have_no_network_or_storage_by_default(): | ||
| for tier in ("ai-generated", "user-uploaded"): | ||
| ceiling = capability_ceiling(tier) | ||
| # No storage, no network, no cross-app data. | ||
| assert "app.kv" not in ceiling | ||
| assert "app.table" not in ceiling | ||
| assert "app.files" not in ceiling | ||
| assert "app.net" not in ceiling | ||
| assert "app.agent" not in ceiling | ||
| assert "app.llm" not in ceiling | ||
| assert "app.memory" not in ceiling | ||
| # Only the inert, app-local UI capabilities are free. | ||
| assert ceiling == frozenset({"app.notify", "app.window"}) | ||
|
|
||
|
|
||
| def test_unknown_is_more_restricted_than_ai_generated_or_user_uploaded(): | ||
| unknown_ceiling = capability_ceiling("unknown") | ||
| assert unknown_ceiling == frozenset() | ||
| for tier in ("ai-generated", "user-uploaded"): | ||
| assert unknown_ceiling < capability_ceiling(tier) | ||
|
|
||
|
|
||
| def test_unrecognised_provenance_falls_back_to_default(): | ||
| assert capability_ceiling("bogus-tier") == capability_ceiling(DEFAULT_PROVENANCE) | ||
| assert capability_ceiling(None) == capability_ceiling(DEFAULT_PROVENANCE) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("cap", sorted(GATED_CAPS)) | ||
| def test_gated_caps_never_free_below_first_party(cap): | ||
| for tier in ("ai-generated", "user-uploaded", "unknown"): | ||
| assert not capability_allowed(tier, cap, granted=[]) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("cap", sorted(FREE_CAPS)) | ||
| def test_first_party_ceiling_covers_every_free_cap(cap): | ||
| assert capability_allowed("first-party", cap, granted=[]) | ||
|
|
||
|
|
||
| def test_capability_allowed_within_ceiling_needs_no_grant(): | ||
| assert capability_allowed("ai-generated", "app.notify", granted=[]) is True | ||
| assert capability_allowed("ai-generated", "app.window.open", granted=[]) is True | ||
|
|
||
|
|
||
| def test_capability_allowed_outside_ceiling_denied_without_grant(): | ||
| assert capability_allowed("ai-generated", "app.kv", granted=[]) is False | ||
| assert capability_allowed("user-uploaded", "app.net", granted=[]) is False | ||
| assert capability_allowed("unknown", "app.notify", granted=[]) is False | ||
|
|
||
|
|
||
| def test_an_explicit_grant_lets_an_app_exceed_its_ceiling(): | ||
| # This is the whole point of the ceiling: it is a default, not a cap. An | ||
| # app can always exceed it via the existing consent/grant flow. | ||
| assert capability_allowed("ai-generated", "app.kv", granted=["app.kv"]) is True | ||
| assert capability_allowed("user-uploaded", "app.net", granted=["app.net"]) is True | ||
| assert capability_allowed("unknown", "app.memory.search", granted=["app.memory"]) is True | ||
| # A grant recorded as the exact sub-capability string also counts. | ||
| assert capability_allowed("unknown", "app.memory.search", granted=["app.memory.search"]) is True | ||
|
|
||
|
|
||
| def test_default_provenance_for_trust_back_compat_mapping(): | ||
| # Legacy first-party built-ins classify as first-party; everything else | ||
| # (community, missing, or any other value) defaults to user-uploaded -- | ||
| # the only other way an app reaches the userspace store today. | ||
| assert default_provenance_for_trust("first-party") == "first-party" | ||
| assert default_provenance_for_trust("community") == "user-uploaded" | ||
| assert default_provenance_for_trust(None) == "user-uploaded" | ||
| assert default_provenance_for_trust("") == "user-uploaded" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING:
hasCapabilityuses an exact-string.includesagainstPROVENANCE_CEILINGS, but the backend'scapability_allowed(tinyagentos/userspace/capabilities.py:176) computes_namespace(capability)(thea.bprefix) and checks that against the ceiling. For a sub-capability likeapp.kv.getthe backend treats it asapp.kvand allows it for first-party apps; this frontend check would sayfalseand the badge would report the app as not having the capability it actually has. Either match the namespace semantics (capability.split('.')[0] + '.' + capability.split('.')[1]) or narrow the docstring to "exact match only -- sub-capabilities not shown".Reply with
@kilocode-bot fix itto have Kilo Code address this issue.