From c4629dd85bcc6938f0e9331f580581b16e7f9fe9 Mon Sep 17 00:00:00 2001 From: Diego Carlino Date: Wed, 22 Jul 2026 03:20:16 +0200 Subject: [PATCH 1/2] Align invoke ordering and protocol semantics --- README.md | 2 +- packages/go/slop-ai/server.go | 19 +- packages/go/slop-ai/server_test.go | 138 +++++++++++++ packages/python/slop-ai/src/slop_ai/server.py | 12 +- packages/python/slop-ai/tests/test_server.py | 115 +++++++++++ packages/python/slop-ai/uv.lock | 152 ++++++++++++++- packages/rust/slop-ai/src/server.rs | 182 ++++++++++++++++-- .../core/__tests__/invoke-ordering.test.ts | 77 ++++++++ packages/typescript/sdk/core/src/provider.ts | 3 + spec/core/attention.md | 24 ++- spec/core/messages.md | 18 +- spec/core/overview.md | 2 +- spec/core/state-tree.md | 6 +- spec/extensions/content-references.md | 6 +- .../src/content/docs/spec/core/attention.md | 24 ++- .../src/content/docs/spec/core/messages.md | 18 +- .../src/content/docs/spec/core/overview.md | 2 +- .../src/content/docs/spec/core/state-tree.md | 6 +- .../spec/extensions/content-references.md | 6 +- 19 files changed, 773 insertions(+), 39 deletions(-) create mode 100644 packages/typescript/sdk/core/__tests__/invoke-ordering.test.ts diff --git a/README.md b/README.md index 70e2c9ff..2d61d3d1 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ SLOP fills the gap: a standard way for apps to **publish what they are** so AI c 1. **State tree** — Apps expose a tree of semantic nodes (not UI elements, not raw data models — meaning). Each node has an identity, properties, and optional children. -2. **Subscriptions and patches** — AI subscribes to subtrees at a chosen depth. The app pushes incremental patches (JSON Patch) as state changes. No polling, no redundant full reads. +2. **Subscriptions and patches** — AI subscribes to subtrees at a chosen depth. The app pushes incremental patches (a JSON-Patch-inspired format with stable node-ID paths) as state changes. No polling, no redundant full reads. 3. **Contextual affordances** — Actions live on the nodes they affect, not in a global tool registry. The AI sees what it can do *in context* — "reply" appears on a message node, "merge" appears on a PR node. diff --git a/packages/go/slop-ai/server.go b/packages/go/slop-ai/server.go index a896fea5..0718e390 100644 --- a/packages/go/slop-ai/server.go +++ b/packages/go/slop-ai/server.go @@ -560,6 +560,11 @@ func (s *Server) handleInvoke(ctx context.Context, conn Connection, msg map[stri result, err := handler.HandleAction(ctx, Params(params)) if err != nil { + // The handler may have mutated state before failing. Per + // spec/core/messages.md §Message ordering, any state changes made by + // the handler MUST be broadcast as patches before the result is sent + // on this connection, regardless of result status. + s.Refresh() if err := conn.Send(map[string]any{ "type": "result", "id": msgID, @@ -598,12 +603,20 @@ func (s *Server) handleInvoke(ctx context.Context, conn Connection, msg map[stri resp["data"] = result } } + // Auto-refresh after invoke — BEFORE sending the result. Per + // spec/core/messages.md §Message ordering, state changes made by an + // invoke handler MUST be broadcast as patch messages to this + // connection's subscriptions before the corresponding result is sent. + // Refresh broadcasts synchronously and every transport's Send is a + // serialized synchronous write, so the patch write to the invoking + // connection completes before the result write below. For "accepted" + // (async) results this covers only changes made before the handler + // returned; later progress arrives in subsequent patches. + s.Refresh() + if err := conn.Send(resp); err != nil { s.logger.Warn("failed to send message", "err", err) } - - // Auto-refresh after invoke - s.Refresh() } // resolveAffordance walks the current tree to find the affordance descriptor diff --git a/packages/go/slop-ai/server_test.go b/packages/go/slop-ai/server_test.go index 9d199588..293e94b9 100644 --- a/packages/go/slop-ai/server_test.go +++ b/packages/go/slop-ai/server_test.go @@ -160,6 +160,144 @@ func TestInvoke(t *testing.T) { } } +// indexOfMessage returns the index of the first message matching type and id +// ("" id matches any), or -1. +func indexOfMessage(msgs []map[string]any, msgType, id string) int { + for i, m := range msgs { + if m["type"] != msgType { + continue + } + if id != "" && m["id"] != id { + continue + } + return i + } + return -1 +} + +// Spec (spec/core/messages.md §Message ordering): patches caused by an invoke +// MUST be sent to the invoking connection before the result message. +func TestInvokePatchBeforeResult(t *testing.T) { + count := 0 + s := NewServer("app", "App") + s.RegisterFunc("counter", func() Node { + return Node{Type: "status", Props: Props{"count": count}} + }) + s.Handle("counter", "increment", HandlerFunc(func(ctx context.Context, p Params) (any, error) { + count++ + return nil, nil + })) + + conn := newMockConn() + s.HandleConnection(conn) + s.HandleMessage(context.Background(), conn, map[string]any{"type": "subscribe", "id": "sub-1"}) + + s.HandleMessage(context.Background(), conn, map[string]any{ + "type": "invoke", + "id": "inv-1", + "path": "/app/counter", + "action": "increment", + }) + + msgs := conn.Messages() + patchIdx := indexOfMessage(msgs, "patch", "") + resultIdx := indexOfMessage(msgs, "result", "inv-1") + if resultIdx == -1 { + t.Fatal("no result message") + } + if msgs[resultIdx]["status"] != "ok" { + t.Fatalf("expected status ok, got %v", msgs[resultIdx]["status"]) + } + if patchIdx == -1 { + t.Fatal("expected a patch message for the state change caused by the invoke") + } + if patchIdx > resultIdx { + t.Fatalf("expected patch (index %d) before result (index %d)", patchIdx, resultIdx) + } +} + +// For "accepted" (async) results, ordering covers changes made before the +// handler returned: those patches must still precede the result. +func TestInvokeAcceptedPatchBeforeResult(t *testing.T) { + status := "idle" + s := NewServer("app", "App") + s.RegisterFunc("job", func() Node { + return Node{Type: "status", Props: Props{"status": status}} + }) + s.Handle("job", "start", HandlerFunc(func(ctx context.Context, p Params) (any, error) { + status = "running" + return map[string]any{"__async": true, "taskId": "task-1"}, nil + })) + + conn := newMockConn() + s.HandleConnection(conn) + s.HandleMessage(context.Background(), conn, map[string]any{"type": "subscribe", "id": "sub-1"}) + + s.HandleMessage(context.Background(), conn, map[string]any{ + "type": "invoke", + "id": "inv-async", + "path": "/app/job", + "action": "start", + }) + + msgs := conn.Messages() + patchIdx := indexOfMessage(msgs, "patch", "") + resultIdx := indexOfMessage(msgs, "result", "inv-async") + if resultIdx == -1 { + t.Fatal("no result message") + } + if msgs[resultIdx]["status"] != "accepted" { + t.Fatalf("expected status accepted, got %v", msgs[resultIdx]["status"]) + } + if patchIdx == -1 { + t.Fatal("expected a patch message for the state change made before the handler returned") + } + if patchIdx > resultIdx { + t.Fatalf("expected patch (index %d) before accepted result (index %d)", patchIdx, resultIdx) + } +} + +// A handler that mutates state and then fails must still have those changes +// broadcast before the error result. +func TestInvokeErrorPatchBeforeResult(t *testing.T) { + attempts := 0 + s := NewServer("app", "App") + s.RegisterFunc("flaky", func() Node { + return Node{Type: "status", Props: Props{"attempts": attempts}} + }) + s.Handle("flaky", "run", HandlerFunc(func(ctx context.Context, p Params) (any, error) { + attempts++ + return nil, context.DeadlineExceeded + })) + + conn := newMockConn() + s.HandleConnection(conn) + s.HandleMessage(context.Background(), conn, map[string]any{"type": "subscribe", "id": "sub-1"}) + + s.HandleMessage(context.Background(), conn, map[string]any{ + "type": "invoke", + "id": "inv-err", + "path": "/app/flaky", + "action": "run", + }) + + msgs := conn.Messages() + patchIdx := indexOfMessage(msgs, "patch", "") + resultIdx := indexOfMessage(msgs, "result", "inv-err") + if resultIdx == -1 { + t.Fatal("no result message") + } + if msgs[resultIdx]["status"] != "error" { + t.Fatalf("expected status error, got %v", msgs[resultIdx]["status"]) + } + if patchIdx == -1 { + t.Fatal("expected a patch message for the state change made before the handler failed") + } + if patchIdx > resultIdx { + t.Fatalf("expected patch (index %d) before error result (index %d)", patchIdx, resultIdx) + } +} + func TestInvokeNotFound(t *testing.T) { s := NewServer("app", "App") conn := newMockConn() diff --git a/packages/python/slop-ai/src/slop_ai/server.py b/packages/python/slop-ai/src/slop_ai/server.py index 213430bf..7fc1a182 100644 --- a/packages/python/slop-ai/src/slop_ai/server.py +++ b/packages/python/slop-ai/src/slop_ai/server.py @@ -431,11 +431,19 @@ async def _handle_invoke( } if result_data: resp["data"] = result_data - conn.send(resp) - # Auto-refresh after invoke + # Spec (core/messages.md, "Message ordering"): patches caused by an + # invoke MUST reach the invoking connection BEFORE the `result`. + # Auto-refresh first so _broadcast_patches writes the patches, then + # send the result. For "accepted" (async) results this covers only + # changes made before the handler returned; later progress arrives + # in subsequent patches. self._rebuild() + conn.send(resp) except Exception as e: + # The handler may have mutated state before raising; per the spec, + # those changes must still be broadcast before the result. + self._rebuild() conn.send({ "type": "result", "id": msg["id"], diff --git a/packages/python/slop-ai/tests/test_server.py b/packages/python/slop-ai/tests/test_server.py index 0a212ba7..8fec9e31 100644 --- a/packages/python/slop-ai/tests/test_server.py +++ b/packages/python/slop-ai/tests/test_server.py @@ -125,6 +125,121 @@ def test_invoke(): assert results[0]["status"] == "ok" +def test_invoke_patches_sent_before_result(): + """Spec (core/messages.md, "Message ordering"): patches caused by an invoke + must arrive on the invoking connection before the `result` message.""" + slop = SlopServer("app", "App") + state = {"count": 0} + + @slop.node("counter") + def counter_node(): + return { + "type": "status", + "props": {"count": state["count"]}, + "actions": {"increment": lambda params: state.update(count=state["count"] + 1)}, + } + + conn = MockConnection() + slop.handle_connection(conn) + _run(slop.handle_message(conn, {"type": "subscribe", "id": "sub-1"})) + + _run(slop.handle_message(conn, { + "type": "invoke", + "id": "inv-1", + "path": "/app/counter", + "action": "increment", + })) + + types = [m["type"] for m in conn.messages] + assert "patch" in types, "invoke that mutates state should produce a patch" + patch_index = types.index("patch") + result_index = types.index("result") + assert patch_index < result_index, "patch must be sent before the result" + + result = conn.messages[result_index] + assert result["status"] == "ok" + # The patch should already reflect the mutation + patch = conn.messages[patch_index] + assert any( + op.get("path") == "/counter/properties/count" and op.get("value") == 1 + for op in patch["ops"] + ) + + +def test_invoke_accepted_patches_sent_before_result(): + """For `accepted` (async) results, changes made before the handler returned + must still be broadcast before the result.""" + slop = SlopServer("app", "App") + state = {"status": "idle"} + + def start(params): + state["status"] = "running" + return {"__async": True, "task_id": "t-1"} + + @slop.node("job") + def job_node(): + return { + "type": "task", + "props": {"status": state["status"]}, + "actions": {"start": start}, + } + + conn = MockConnection() + slop.handle_connection(conn) + _run(slop.handle_message(conn, {"type": "subscribe", "id": "sub-1"})) + + _run(slop.handle_message(conn, { + "type": "invoke", + "id": "inv-1", + "path": "/app/job", + "action": "start", + })) + + types = [m["type"] for m in conn.messages] + assert "patch" in types + assert types.index("patch") < types.index("result") + result = [m for m in conn.messages if m["type"] == "result"][0] + assert result["status"] == "accepted" + assert result["data"] == {"task_id": "t-1"} + + +def test_invoke_error_patches_sent_before_result(): + """A handler that mutates state and then raises: those changes must still be + broadcast before the error result.""" + slop = SlopServer("app", "App") + state = {"attempts": 0} + + def flaky(params): + state["attempts"] += 1 + raise RuntimeError("boom") + + @slop.node("thing") + def thing_node(): + return { + "type": "status", + "props": {"attempts": state["attempts"]}, + "actions": {"try": flaky}, + } + + conn = MockConnection() + slop.handle_connection(conn) + _run(slop.handle_message(conn, {"type": "subscribe", "id": "sub-1"})) + + _run(slop.handle_message(conn, { + "type": "invoke", + "id": "inv-1", + "path": "/app/thing", + "action": "try", + })) + + types = [m["type"] for m in conn.messages] + assert "patch" in types + assert types.index("patch") < types.index("result") + result = [m for m in conn.messages if m["type"] == "result"][0] + assert result["status"] == "error" + assert "boom" in result["error"]["message"] + + def test_invoke_not_found(): slop = SlopServer("app", "App") conn = MockConnection() diff --git a/packages/python/slop-ai/uv.lock b/packages/python/slop-ai/uv.lock index 6d7479d0..28215b8e 100644 --- a/packages/python/slop-ai/uv.lock +++ b/packages/python/slop-ai/uv.lock @@ -2,6 +2,81 @@ version = 1 revision = 3 requires-python = ">=3.10" +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "slop-ai" version = "0.1.0" @@ -9,18 +84,93 @@ source = { editable = "." } [package.optional-dependencies] all = [ + { name = "pytest" }, { name = "websockets" }, ] +dev = [ + { name = "pytest" }, + { name = "websockets" }, +] +test = [ + { name = "pytest" }, +] websocket = [ { name = "websockets" }, ] [package.metadata] requires-dist = [ + { name = "pytest", marker = "extra == 'all'", specifier = ">=9.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=9.0" }, { name = "websockets", marker = "extra == 'all'", specifier = ">=12.0" }, + { name = "websockets", marker = "extra == 'dev'", specifier = ">=12.0" }, { name = "websockets", marker = "extra == 'websocket'", specifier = ">=12.0" }, ] -provides-extras = ["websocket", "all"] +provides-extras = ["test", "dev", "websocket", "all"] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] [[package]] name = "websockets" diff --git a/packages/rust/slop-ai/src/server.rs b/packages/rust/slop-ai/src/server.rs index 837d88e7..a70e420f 100644 --- a/packages/rust/slop-ai/src/server.rs +++ b/packages/rust/slop-ai/src/server.rs @@ -575,7 +575,7 @@ impl SlopServer { return; } } - match h(¶ms) { + let resp = match h(¶ms) { Ok(data) => { let is_async = data .as_ref() @@ -599,19 +599,27 @@ impl SlopServer { } } } - let _ = conn.send(&resp); + resp } - Err(e) => { - let _ = conn.send(&json!({ - "type": "result", - "id": msg_id, - "status": "error", - "error": {"code": "internal", "message": e.to_string()} - })); - } - } - // Auto-refresh after invoke + Err(e) => json!({ + "type": "result", + "id": msg_id, + "status": "error", + "error": {"code": "internal", "message": e.to_string()} + }), + }; + // Auto-refresh after invoke, BEFORE sending the result. + // Spec (spec/core/messages.md "Message ordering"): patch + // messages caused by an invoke MUST reach the invoking + // connection before the corresponding `result` — regardless + // of ok/error/accepted status. For `accepted` (async) results + // this covers only changes made before the handler returned; + // later progress arrives in subsequent patches. Every + // transport delivers messages in `Connection::send` order + // (per-connection mpsc or locked writer), so broadcasting + // patches first guarantees the wire ordering. self.refresh(); + let _ = conn.send(&resp); } } } @@ -1028,6 +1036,156 @@ mod tests { assert_eq!(*state.lock().unwrap(), 1); } + /// Spec (spec/core/messages.md "Message ordering"): patches caused by an + /// invoke MUST arrive on the invoking connection before the `result`. + #[test] + fn test_invoke_patch_arrives_before_result() { + let state = Arc::new(Mutex::new(0i64)); + let slop = SlopServer::new("app", "App"); + + let s = state.clone(); + slop.register_fn("counter", move || { + let n = *s.lock().unwrap(); + json!({"type": "status", "props": {"count": n}}) + }); + + let s = state.clone(); + slop.action("counter", "increment", move |_params: &Value| { + *s.lock().unwrap() += 1; + Ok(None) + }); + + let conn = MockConnection::new(); + let dyn_conn = as_dyn(&conn); + slop.handle_connection(dyn_conn.clone()); + slop.handle_message(&dyn_conn, &json!({"type": "subscribe", "id": "sub-1"})); + slop.handle_message( + &dyn_conn, + &json!({ + "type": "invoke", + "id": "inv-1", + "path": "/app/counter", + "action": "increment" + }), + ); + + let messages = conn.messages(); + let patch_idx = messages + .iter() + .position(|m| m["type"] == "patch") + .expect("invoke that mutates state must produce a patch"); + let result_idx = messages + .iter() + .position(|m| m["type"] == "result" && m["id"] == "inv-1") + .expect("invoke must produce a result"); + assert!( + patch_idx < result_idx, + "patch (index {patch_idx}) must arrive before result (index {result_idx})" + ); + assert_eq!(messages[result_idx]["status"], "ok"); + // The patch must already reflect the handler's state change. + let ops = messages[patch_idx]["ops"].as_array().unwrap(); + assert!(ops.iter().any(|op| op["value"]["props"]["count"] == 1 + || op["value"]["count"] == 1 + || op["value"] == 1)); + } + + /// Same ordering rule for `accepted` (async) results: changes made before + /// the handler returned must be broadcast before the result. + #[test] + fn test_invoke_patch_before_accepted_result() { + let state = Arc::new(Mutex::new(0i64)); + let slop = SlopServer::new("app", "App"); + + let s = state.clone(); + slop.register_fn("job", move || { + let n = *s.lock().unwrap(); + json!({"type": "status", "props": {"runs": n}}) + }); + + let s = state.clone(); + slop.action("job", "start", move |_params: &Value| { + *s.lock().unwrap() += 1; + Ok(Some(json!({"__async": true, "taskId": "task-1"}))) + }); + + let conn = MockConnection::new(); + let dyn_conn = as_dyn(&conn); + slop.handle_connection(dyn_conn.clone()); + slop.handle_message(&dyn_conn, &json!({"type": "subscribe", "id": "sub-1"})); + slop.handle_message( + &dyn_conn, + &json!({ + "type": "invoke", + "id": "inv-async", + "path": "/app/job", + "action": "start" + }), + ); + + let messages = conn.messages(); + let patch_idx = messages + .iter() + .position(|m| m["type"] == "patch") + .expect("state change before handler return must produce a patch"); + let result_idx = messages + .iter() + .position(|m| m["type"] == "result" && m["id"] == "inv-async") + .expect("invoke must produce a result"); + assert!(patch_idx < result_idx); + assert_eq!(messages[result_idx]["status"], "accepted"); + assert_eq!(messages[result_idx]["data"]["taskId"], "task-1"); + } + + /// A handler that mutates state and then fails still broadcasts its + /// patches before the error result. + #[test] + fn test_invoke_patch_before_error_result() { + let state = Arc::new(Mutex::new(0i64)); + let slop = SlopServer::new("app", "App"); + + let s = state.clone(); + slop.register_fn("flaky", move || { + let n = *s.lock().unwrap(); + json!({"type": "status", "props": {"attempts": n}}) + }); + + let s = state.clone(); + slop.action("flaky", "try", move |_params: &Value| { + *s.lock().unwrap() += 1; + Err(crate::error::SlopError::ActionFailed { + code: "internal".into(), + message: "boom".into(), + }) + }); + + let conn = MockConnection::new(); + let dyn_conn = as_dyn(&conn); + slop.handle_connection(dyn_conn.clone()); + slop.handle_message(&dyn_conn, &json!({"type": "subscribe", "id": "sub-1"})); + slop.handle_message( + &dyn_conn, + &json!({ + "type": "invoke", + "id": "inv-err", + "path": "/app/flaky", + "action": "try" + }), + ); + + let messages = conn.messages(); + let patch_idx = messages + .iter() + .position(|m| m["type"] == "patch") + .expect("state change must produce a patch even when the handler errors"); + let result_idx = messages + .iter() + .position(|m| m["type"] == "result" && m["id"] == "inv-err") + .expect("invoke must produce a result"); + assert!(patch_idx < result_idx); + assert_eq!(messages[result_idx]["status"], "error"); + } + #[test] fn test_invoke_not_found() { let slop = SlopServer::new("app", "App"); diff --git a/packages/typescript/sdk/core/__tests__/invoke-ordering.test.ts b/packages/typescript/sdk/core/__tests__/invoke-ordering.test.ts new file mode 100644 index 00000000..30503fad --- /dev/null +++ b/packages/typescript/sdk/core/__tests__/invoke-ordering.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "bun:test"; +import { ProviderBase } from "../src/provider"; +import type { NodeDescriptor, PatchOp } from "../src/types"; + +class RecordingProvider extends ProviderBase { + broadcasts: PatchOp[][] = []; + private registrations = new Map NodeDescriptor)>(); + + constructor(regs: Record NodeDescriptor)>) { + super({ id: "fake", name: "Fake" }); + for (const [p, d] of Object.entries(regs)) this.registrations.set(p, d); + (this as any).rebuild(); + } + // Evaluate descriptor functions on every rebuild, like the server does. + protected getRegistrations() { + const out = new Map(); + for (const [p, d] of this.registrations) out.set(p, typeof d === "function" ? d() : d); + return out; + } + protected broadcast(ops: PatchOp[]) { + this.broadcasts.push(ops); + } +} + +describe("invoke ordering (spec/core/messages.md §Message ordering)", () => { + it("broadcasts handler state changes before returning an ok result", async () => { + let count = 0; + const p = new RecordingProvider({ + counter: () => + ({ + type: "status", + props: { count }, + actions: { + bump: () => { + count += 1; + }, + }, + }) as NodeDescriptor, + }); + const before = p.broadcasts.length; + const r = (await p.executeInvoke({ + id: "inv-1", + path: "/fake/counter", + action: "bump", + })) as any; + expect(r.status).toBe("ok"); + // rebuild() ran before the result was produced, so the mutation's patch + // ops were already handed to the transport layer. + expect(p.broadcasts.length).toBeGreaterThan(before); + }); + + it("broadcasts state changes made before a handler error, before the error result", async () => { + let count = 0; + const p = new RecordingProvider({ + counter: () => + ({ + type: "status", + props: { count }, + actions: { + bump_then_fail: () => { + count += 1; + throw new Error("boom"); + }, + }, + }) as NodeDescriptor, + }); + const before = p.broadcasts.length; + const r = (await p.executeInvoke({ + id: "inv-2", + path: "/fake/counter", + action: "bump_then_fail", + })) as any; + expect(r.status).toBe("error"); + expect(r.error.message).toBe("boom"); + expect(p.broadcasts.length).toBeGreaterThan(before); + }); +}); diff --git a/packages/typescript/sdk/core/src/provider.ts b/packages/typescript/sdk/core/src/provider.ts index a2220662..cad72561 100644 --- a/packages/typescript/sdk/core/src/provider.ts +++ b/packages/typescript/sdk/core/src/provider.ts @@ -194,6 +194,9 @@ export abstract class ProviderBase { this.rebuild(); return result; } catch (err: any) { + // A handler may mutate state before failing; those changes MUST still be + // broadcast before the error result (spec/core/messages.md §Message ordering). + this.rebuild(); return { type: "result", id: msg.id, diff --git a/spec/core/attention.md b/spec/core/attention.md index 29e92191..5b26ab04 100644 --- a/spec/core/attention.md +++ b/spec/core/attention.md @@ -38,6 +38,15 @@ A score indicating how relevant this node is to the current moment. Providers co Salience is relative within a tree. It helps the consumer decide *what to read first*, not whether something exists. +#### Default salience + +`meta.salience` is optional. Wherever a salience value is consumed, implementations MUST treat a node without a score as having salience **0.5**: + +- **Filtering** — a node without `meta.salience` passes a `filter.min_salience` threshold exactly when `0.5 >= min_salience`. (A node is excluded when its effective salience is strictly less than the threshold; equality passes.) +- **Compaction** — the compaction scoring formula (see [Optimization pipeline](#optimization-pipeline)) uses `0.5` as the salience of unscored nodes. + +The default is deliberately mid-range: unscored nodes survive permissive filters (`min_salience <= 0.5`) but are dropped by stricter ones. A provider that needs a node to survive aggressive filtering MUST score it explicitly (or mark it `pinned`). + ### `pinned` (boolean) When `true`, this node and its children must never be collapsed by auto-compaction, regardless of salience score or node budget. Use for subtrees that should always be visible to the AI — such as the `ui` node in fullstack apps, or critical status indicators. @@ -46,7 +55,14 @@ Default: `false`. ### `changed` (boolean) -Set to `true` on nodes that were modified in the most recent patch. Automatically cleared on the next patch cycle. This lets the consumer quickly scan for what's new without diffing. +Set to `true` on nodes that were modified in the most recent patch. This lets the consumer quickly scan for what's new without diffing. + +**Clearing semantics (clarification).** `changed` is transient — it describes only the most recent patch, and clearing happens on the consumer side: + +- Consumers MUST treat `changed: true` as scoped to the patch (or snapshot) that delivered it. When the next patch arrives on the same subscription, `changed` flags from earlier patches are stale and MUST be disregarded (or cleared in the local mirror), whether or not the new patch touches those nodes. +- Providers MUST NOT emit patch ops whose sole purpose is to clear a stale `changed` flag, and consumers MUST NOT rely on receiving such clear-ops. Emitting explicit clears would roughly double patch traffic without adding information. + +A provider MAY still set `changed: false` (or remove the flag) as part of a larger `meta` diff; consumers apply that like any other meta update. ### `focus` (boolean) @@ -156,7 +172,7 @@ This gives the AI a quick "what's going on?" read in minimal tokens, from which - **Update salience as context changes.** A node's salience should reflect the current moment, not a static priority. - **Use `reason` generously.** It's cheap (one string) and extremely valuable for AI comprehension. - **Set `focus` based on actual user interaction**, not guesses. It should reflect what the user is looking at or typing into right now. -- **`changed` should auto-clear.** It marks the *last* change, not all historical changes. +- **`changed` marks the *last* change, not all historical changes.** Clearing is consumer-side — do not emit patch ops just to clear it (see [`changed`](#changed-boolean)). ## Tree optimization @@ -215,9 +231,9 @@ The provider MAY enforce a global ceiling (e.g., never send more than 500 nodes Providers MUST apply optimizations in this order: -1. **Filter** — Remove nodes below `filter.min_salience` or not in `filter.types`. The root is never filtered. +1. **Filter** — Remove nodes below `filter.min_salience` or not in `filter.types`. The root is never filtered. Nodes without `meta.salience` use the default of 0.5 (see [Default salience](#default-salience)). 2. **Truncate** — Collapse nodes beyond `depth` into stubs with `meta.total_children` and `meta.summary`. -3. **Compact** — If the tree still exceeds `max_nodes`, collapse lowest-salience subtrees first. Scoring: `score = salience - depth × 0.01 - childCount × 0.001`. Root children and pinned nodes are never collapsed. +3. **Compact** — If the tree still exceeds `max_nodes`, collapse lowest-salience subtrees first. Scoring: `score = salience - depth × 0.01 - childCount × 0.001`, where `salience` is 0.5 for unscored nodes (see [Default salience](#default-salience)). Root children and pinned nodes are never collapsed. 4. **Window** — For `query` messages with `window`, slice the requested node's children and set `meta.window` and `meta.total_children`. Each step is optional and only applies when the corresponding parameter is present. diff --git a/spec/core/messages.md b/spec/core/messages.md index 393cf3aa..6da04b8c 100644 --- a/spec/core/messages.md +++ b/spec/core/messages.md @@ -14,6 +14,19 @@ Every message has a `type` field and optionally an `id` for request-response cor } ``` +### Correlation and identity + +The `id` field is *usually* a one-shot correlation id, but a few message types intentionally overload it. These special cases are deliberate and the wire field names MUST NOT change: + +| Message | Field | Meaning | +|---|---|---| +| `subscribe` | `id` | Correlation id **and** the durable subscription identity. Echoed as `id` on the initial `snapshot`, and as `subscription` on every subsequent `patch`. MUST be unique among the consumer's active subscriptions. | +| `unsubscribe` | `id` | The subscription to cancel — not a fresh correlation id. `unsubscribe` has no correlation id of its own. | +| `query` / `invoke` | `id` | One-shot correlation; echoed as `id` on the `snapshot` / `result` response. | +| `snapshot` | `id` | The `subscribe` or `query` this answers (for subscriptions, the subscription identity). | +| `patch` | `subscription` | The subscription this patch applies to. Named `subscription` rather than `id` because a patch is a stream element, not a response to a single request. | +| `error` | `id` | The `id` of the offending consumer message, when known. | + ## Consumer → Provider messages ### `subscribe` @@ -29,7 +42,7 @@ Begin observing a subtree. The provider responds with a `snapshot` and then stre "max_nodes": 200, // Maximum total nodes in the snapshot (optional) "filter": { // Optional filters "types": ["item", "notification"], // Only include these node types - "min_salience": 0.5 // Only include nodes above this salience + "min_salience": 0.5 // Only include nodes with salience >= this value } } ``` @@ -280,7 +293,8 @@ If the error is not associated with a specific consumer message (e.g., internal - Messages within a subscription are strictly ordered: `snapshot` before any `patch`, patches in version order. - Messages across subscriptions have no ordering guarantee. -- `result` messages for `invoke` may arrive interleaved with patches. The `id` field correlates responses. +- `result` messages for `invoke` may arrive interleaved with patches from unrelated state changes. The `id` field correlates responses. +- **Invoke results and their patches:** state changes made by an invoke handler MUST be broadcast — as `patch` (or re-base `snapshot`) messages to the connection's affected subscriptions — *before* the provider sends the corresponding `result` on that connection. A consumer that processes messages in receive order can therefore assume its local mirror already reflects the action's effects by the time it reads an `ok` result; consumers SHOULD apply all patches received ahead of a result before re-reading affected state. For `accepted` (async) results, this ordering covers only changes made before the handler returned — subsequent progress arrives in later patches. ## Batch messages diff --git a/spec/core/overview.md b/spec/core/overview.md index d8c22b2a..07e6b85b 100644 --- a/spec/core/overview.md +++ b/spec/core/overview.md @@ -33,7 +33,7 @@ The key properties: | **Node** | A single element in the state tree, with an ID, type, properties, children, and affordances | | **Affordance** | An action available on a node — contextual, not global | | **Subscription** | A consumer's request to observe a subtree at a given depth | -| **Patch** | An incremental update to the state tree (JSON Patch format) | +| **Patch** | An incremental update to the state tree (SLOP patch format, modeled on JSON Patch) | | **Salience** | A hint from the provider about how important/relevant a node is right now | | **Depth** | How many levels deep into the tree a query or subscription resolves. `0` = this node only, `1` = this node + direct children, `-1` = unlimited | diff --git a/spec/core/state-tree.md b/spec/core/state-tree.md index 35a7dc4c..40a0b8c1 100644 --- a/spec/core/state-tree.md +++ b/spec/core/state-tree.md @@ -7,7 +7,7 @@ The state tree is the core data structure of SLOP. It is a rooted tree of **node ```jsonc { // REQUIRED - "id": "msg-42", // Stable identifier, unique within the tree + "id": "msg-42", // Stable identifier, unique among siblings "type": "message", // Semantic type (see type taxonomy below) // OPTIONAL @@ -27,7 +27,7 @@ The state tree is the core data structure of SLOP. It is a rooted tree of **node ### `id` -A string that uniquely identifies a node within the tree. Must be stable across patches — if a node's `id` changes, it's a different node. IDs are opaque to consumers; providers choose the format. +A string that uniquely identifies a node among its siblings. Must be stable across patches — if a node's `id` changes, it's a different node. IDs are opaque to consumers; providers choose the format. **Requirements:** - Unique among siblings (no two children of the same parent share an ID) @@ -36,6 +36,8 @@ A string that uniquely identifies a node within the tree. Must be stable across - MUST NOT equal any reserved node-field keyword: `properties`, `children`, `affordances`, `meta`, `content_ref`, `id`, or `type`. These keywords have structural meaning in patch paths (see [Messages](./messages.md#patch-path-syntax)), so an id colliding with one is unaddressable. - MUST NOT contain the characters `/` (U+002F) or `~` (U+007E). Both are reserved in patch paths: `/` is the path segment delimiter and `~` is the JSON Pointer escape prefix (see [Messages](./messages.md#patch-path-syntax)). Providers that generate IDs from external sources (URLs, file paths, external IDs) MUST escape, hash, or otherwise transform those characters before using the value as a SLOP id. +Because every node's `id` is unique among its siblings, the sequence of ids from the root uniquely addresses any node in the tree — this is what patch paths rely on (see [Messages](./messages.md#patch-path-syntax)). The same id may appear under different parents. + ### `type` A string describing what kind of thing this node represents. Types are semantic, not structural — they describe meaning, not UI. diff --git a/spec/extensions/content-references.md b/spec/extensions/content-references.md index bde562b5..518dfe89 100644 --- a/spec/extensions/content-references.md +++ b/spec/extensions/content-references.md @@ -238,12 +238,16 @@ slop.register("inbox/msg-42", { }, }, actions: { - read_body: () => ({ content: message.body, encoding: "utf-8" }), + read_content: () => ({ content: message.body, encoding: "utf-8" }), reply: { params: { body: "string" }, handler: ({ body }) => sendReply(body as string) }, }, }); ``` +The body's `contentRef` omits `uri`, so it auto-generates `slop://content/inbox/msg-42` — a `slop://` URI MUST be backed by a `read_content` affordance on the node that carries the `content_ref`, hence the action name above. The attachment supplies an explicit `https://` URI, so it needs no `read_content` of its own. + +**Disambiguation.** When a node and its descendants each carry content references, an auto-generated `slop://` URI always resolves via the `read_content` affordance of *the node carrying that `content_ref`* — never a parent's or child's. Each child node's reference resolves through its own node's `read_content` affordance, or through its explicit `uri` when one is set. + ## Streaming content For content that grows over time (logs, terminal output), use `type: "stream"`: diff --git a/website/docs/src/content/docs/spec/core/attention.md b/website/docs/src/content/docs/spec/core/attention.md index 2a091c6d..21bfde79 100644 --- a/website/docs/src/content/docs/spec/core/attention.md +++ b/website/docs/src/content/docs/spec/core/attention.md @@ -39,6 +39,15 @@ A score indicating how relevant this node is to the current moment. Providers co Salience is relative within a tree. It helps the consumer decide *what to read first*, not whether something exists. +#### Default salience + +`meta.salience` is optional. Wherever a salience value is consumed, implementations MUST treat a node without a score as having salience **0.5**: + +- **Filtering** — a node without `meta.salience` passes a `filter.min_salience` threshold exactly when `0.5 >= min_salience`. (A node is excluded when its effective salience is strictly less than the threshold; equality passes.) +- **Compaction** — the compaction scoring formula (see [Optimization pipeline](#optimization-pipeline)) uses `0.5` as the salience of unscored nodes. + +The default is deliberately mid-range: unscored nodes survive permissive filters (`min_salience <= 0.5`) but are dropped by stricter ones. A provider that needs a node to survive aggressive filtering MUST score it explicitly (or mark it `pinned`). + ### `pinned` (boolean) When `true`, this node and its children must never be collapsed by auto-compaction, regardless of salience score or node budget. Use for subtrees that should always be visible to the AI — such as the `ui` node in fullstack apps, or critical status indicators. @@ -47,7 +56,14 @@ Default: `false`. ### `changed` (boolean) -Set to `true` on nodes that were modified in the most recent patch. Automatically cleared on the next patch cycle. This lets the consumer quickly scan for what's new without diffing. +Set to `true` on nodes that were modified in the most recent patch. This lets the consumer quickly scan for what's new without diffing. + +**Clearing semantics (clarification).** `changed` is transient — it describes only the most recent patch, and clearing happens on the consumer side: + +- Consumers MUST treat `changed: true` as scoped to the patch (or snapshot) that delivered it. When the next patch arrives on the same subscription, `changed` flags from earlier patches are stale and MUST be disregarded (or cleared in the local mirror), whether or not the new patch touches those nodes. +- Providers MUST NOT emit patch ops whose sole purpose is to clear a stale `changed` flag, and consumers MUST NOT rely on receiving such clear-ops. Emitting explicit clears would roughly double patch traffic without adding information. + +A provider MAY still set `changed: false` (or remove the flag) as part of a larger `meta` diff; consumers apply that like any other meta update. ### `focus` (boolean) @@ -157,7 +173,7 @@ This gives the AI a quick "what's going on?" read in minimal tokens, from which - **Update salience as context changes.** A node's salience should reflect the current moment, not a static priority. - **Use `reason` generously.** It's cheap (one string) and extremely valuable for AI comprehension. - **Set `focus` based on actual user interaction**, not guesses. It should reflect what the user is looking at or typing into right now. -- **`changed` should auto-clear.** It marks the *last* change, not all historical changes. +- **`changed` marks the *last* change, not all historical changes.** Clearing is consumer-side — do not emit patch ops just to clear it (see [`changed`](#changed-boolean)). ## Tree optimization @@ -216,9 +232,9 @@ The provider MAY enforce a global ceiling (e.g., never send more than 500 nodes Providers MUST apply optimizations in this order: -1. **Filter** — Remove nodes below `filter.min_salience` or not in `filter.types`. The root is never filtered. +1. **Filter** — Remove nodes below `filter.min_salience` or not in `filter.types`. The root is never filtered. Nodes without `meta.salience` use the default of 0.5 (see [Default salience](#default-salience)). 2. **Truncate** — Collapse nodes beyond `depth` into stubs with `meta.total_children` and `meta.summary`. -3. **Compact** — If the tree still exceeds `max_nodes`, collapse lowest-salience subtrees first. Scoring: `score = salience - depth × 0.01 - childCount × 0.001`. Root children and pinned nodes are never collapsed. +3. **Compact** — If the tree still exceeds `max_nodes`, collapse lowest-salience subtrees first. Scoring: `score = salience - depth × 0.01 - childCount × 0.001`, where `salience` is 0.5 for unscored nodes (see [Default salience](#default-salience)). Root children and pinned nodes are never collapsed. 4. **Window** — For `query` messages with `window`, slice the requested node's children and set `meta.window` and `meta.total_children`. Each step is optional and only applies when the corresponding parameter is present. diff --git a/website/docs/src/content/docs/spec/core/messages.md b/website/docs/src/content/docs/spec/core/messages.md index 43551c75..a4fca412 100644 --- a/website/docs/src/content/docs/spec/core/messages.md +++ b/website/docs/src/content/docs/spec/core/messages.md @@ -15,6 +15,19 @@ Every message has a `type` field and optionally an `id` for request-response cor } ``` +### Correlation and identity + +The `id` field is *usually* a one-shot correlation id, but a few message types intentionally overload it. These special cases are deliberate and the wire field names MUST NOT change: + +| Message | Field | Meaning | +|---|---|---| +| `subscribe` | `id` | Correlation id **and** the durable subscription identity. Echoed as `id` on the initial `snapshot`, and as `subscription` on every subsequent `patch`. MUST be unique among the consumer's active subscriptions. | +| `unsubscribe` | `id` | The subscription to cancel — not a fresh correlation id. `unsubscribe` has no correlation id of its own. | +| `query` / `invoke` | `id` | One-shot correlation; echoed as `id` on the `snapshot` / `result` response. | +| `snapshot` | `id` | The `subscribe` or `query` this answers (for subscriptions, the subscription identity). | +| `patch` | `subscription` | The subscription this patch applies to. Named `subscription` rather than `id` because a patch is a stream element, not a response to a single request. | +| `error` | `id` | The `id` of the offending consumer message, when known. | + ## Consumer → Provider messages ### `subscribe` @@ -30,7 +43,7 @@ Begin observing a subtree. The provider responds with a `snapshot` and then stre "max_nodes": 200, // Maximum total nodes in the snapshot (optional) "filter": { // Optional filters "types": ["item", "notification"], // Only include these node types - "min_salience": 0.5 // Only include nodes above this salience + "min_salience": 0.5 // Only include nodes with salience >= this value } } ``` @@ -281,7 +294,8 @@ If the error is not associated with a specific consumer message (e.g., internal - Messages within a subscription are strictly ordered: `snapshot` before any `patch`, patches in version order. - Messages across subscriptions have no ordering guarantee. -- `result` messages for `invoke` may arrive interleaved with patches. The `id` field correlates responses. +- `result` messages for `invoke` may arrive interleaved with patches from unrelated state changes. The `id` field correlates responses. +- **Invoke results and their patches:** state changes made by an invoke handler MUST be broadcast — as `patch` (or re-base `snapshot`) messages to the connection's affected subscriptions — *before* the provider sends the corresponding `result` on that connection. A consumer that processes messages in receive order can therefore assume its local mirror already reflects the action's effects by the time it reads an `ok` result; consumers SHOULD apply all patches received ahead of a result before re-reading affected state. For `accepted` (async) results, this ordering covers only changes made before the handler returned — subsequent progress arrives in later patches. ## Batch messages diff --git a/website/docs/src/content/docs/spec/core/overview.md b/website/docs/src/content/docs/spec/core/overview.md index 48491ea8..75ea850d 100644 --- a/website/docs/src/content/docs/spec/core/overview.md +++ b/website/docs/src/content/docs/spec/core/overview.md @@ -34,7 +34,7 @@ The key properties: | **Node** | A single element in the state tree, with an ID, type, properties, children, and affordances | | **Affordance** | An action available on a node — contextual, not global | | **Subscription** | A consumer's request to observe a subtree at a given depth | -| **Patch** | An incremental update to the state tree (JSON Patch format) | +| **Patch** | An incremental update to the state tree (SLOP patch format, modeled on JSON Patch) | | **Salience** | A hint from the provider about how important/relevant a node is right now | | **Depth** | How many levels deep into the tree a query or subscription resolves. `0` = this node only, `1` = this node + direct children, `-1` = unlimited | diff --git a/website/docs/src/content/docs/spec/core/state-tree.md b/website/docs/src/content/docs/spec/core/state-tree.md index 4d0d9bb6..82366f8d 100644 --- a/website/docs/src/content/docs/spec/core/state-tree.md +++ b/website/docs/src/content/docs/spec/core/state-tree.md @@ -8,7 +8,7 @@ The state tree is the core data structure of SLOP. It is a rooted tree of **node ```jsonc { // REQUIRED - "id": "msg-42", // Stable identifier, unique within the tree + "id": "msg-42", // Stable identifier, unique among siblings "type": "message", // Semantic type (see type taxonomy below) // OPTIONAL @@ -28,7 +28,7 @@ The state tree is the core data structure of SLOP. It is a rooted tree of **node ### `id` -A string that uniquely identifies a node within the tree. Must be stable across patches — if a node's `id` changes, it's a different node. IDs are opaque to consumers; providers choose the format. +A string that uniquely identifies a node among its siblings. Must be stable across patches — if a node's `id` changes, it's a different node. IDs are opaque to consumers; providers choose the format. **Requirements:** - Unique among siblings (no two children of the same parent share an ID) @@ -37,6 +37,8 @@ A string that uniquely identifies a node within the tree. Must be stable across - MUST NOT equal any reserved node-field keyword: `properties`, `children`, `affordances`, `meta`, `content_ref`, `id`, or `type`. These keywords have structural meaning in patch paths (see [Messages](/spec/core/messages#patch-path-syntax)), so an id colliding with one is unaddressable. - MUST NOT contain the characters `/` (U+002F) or `~` (U+007E). Both are reserved in patch paths: `/` is the path segment delimiter and `~` is the JSON Pointer escape prefix (see [Messages](/spec/core/messages#patch-path-syntax)). Providers that generate IDs from external sources (URLs, file paths, external IDs) MUST escape, hash, or otherwise transform those characters before using the value as a SLOP id. +Because every node's `id` is unique among its siblings, the sequence of ids from the root uniquely addresses any node in the tree — this is what patch paths rely on (see [Messages](/spec/core/messages#patch-path-syntax)). The same id may appear under different parents. + ### `type` A string describing what kind of thing this node represents. Types are semantic, not structural — they describe meaning, not UI. diff --git a/website/docs/src/content/docs/spec/extensions/content-references.md b/website/docs/src/content/docs/spec/extensions/content-references.md index 8698945a..4b96c30c 100644 --- a/website/docs/src/content/docs/spec/extensions/content-references.md +++ b/website/docs/src/content/docs/spec/extensions/content-references.md @@ -239,12 +239,16 @@ slop.register("inbox/msg-42", { }, }, actions: { - read_body: () => ({ content: message.body, encoding: "utf-8" }), + read_content: () => ({ content: message.body, encoding: "utf-8" }), reply: { params: { body: "string" }, handler: ({ body }) => sendReply(body as string) }, }, }); ``` +The body's `contentRef` omits `uri`, so it auto-generates `slop://content/inbox/msg-42` — a `slop://` URI MUST be backed by a `read_content` affordance on the node that carries the `content_ref`, hence the action name above. The attachment supplies an explicit `https://` URI, so it needs no `read_content` of its own. + +**Disambiguation.** When a node and its descendants each carry content references, an auto-generated `slop://` URI always resolves via the `read_content` affordance of *the node carrying that `content_ref`* — never a parent's or child's. Each child node's reference resolves through its own node's `read_content` affordance, or through its explicit `uri` when one is set. + ## Streaming content For content that grows over time (logs, terminal output), use `type: "stream"`: From 2f6e2fedac7f66601ca04a6df1c7356805f4132b Mon Sep 17 00:00:00 2001 From: Diego Carlino Date: Wed, 22 Jul 2026 03:20:16 +0200 Subject: [PATCH 2/2] Harden desktop bridge discovery and pairing --- apps/desktop/src-tauri/Cargo.lock | 3 + apps/desktop/src-tauri/Cargo.toml | 3 + .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 60 +++- .../src-tauri/gen/schemas/macOS-schema.json | 60 +++- apps/desktop/src-tauri/src/bridge/mod.rs | 172 ++++++++++- apps/desktop/src-tauri/src/bridge/security.rs | 286 ++++++++++++++++++ apps/desktop/src-tauri/src/commands.rs | 18 ++ apps/desktop/src-tauri/src/lib.rs | 6 + apps/desktop/src/App.css | 75 +++++ apps/desktop/src/components/Settings.tsx | 67 +++- apps/desktop/src/lib/commands.ts | 4 +- apps/extension/__tests__/bridge-auth.test.ts | 24 ++ apps/extension/__tests__/origin.test.ts | 84 +++++ apps/extension/package.json | 3 +- apps/extension/popup.html | 67 ++++ .../extension/src/background/bridge-client.ts | 118 +++++++- apps/extension/src/background/index.ts | 46 ++- apps/extension/src/background/session.ts | 32 +- apps/extension/src/background/tab-registry.ts | 219 +++++++++++--- apps/extension/src/env.d.ts | 5 + apps/extension/src/lib/approvals.ts | 29 ++ apps/extension/src/lib/bridge-auth.ts | 44 +++ apps/extension/src/lib/origin.ts | 72 +++++ apps/extension/src/popup/popup.ts | 170 ++++++++++- apps/extension/src/types.ts | 43 +++ spec/core/transport.md | 10 +- spec/integrations/desktop.md | 10 +- .../src/content/docs/spec/core/transport.md | 10 +- .../content/docs/spec/integrations/desktop.md | 10 +- 30 files changed, 1664 insertions(+), 88 deletions(-) create mode 100644 apps/desktop/src-tauri/src/bridge/security.rs create mode 100644 apps/extension/__tests__/bridge-auth.test.ts create mode 100644 apps/extension/__tests__/origin.test.ts create mode 100644 apps/extension/src/lib/approvals.ts create mode 100644 apps/extension/src/lib/bridge-auth.ts create mode 100644 apps/extension/src/lib/origin.ts diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index d3b03dc2..08c7761a 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -3446,13 +3446,16 @@ name = "slop-desktop" version = "0.1.0" dependencies = [ "async-trait", + "base64 0.22.1", "dirs", "futures-util", + "getrandom 0.3.4", "notify", "reqwest 0.12.28", "serde", "serde_json", "slop-ai", + "subtle", "tauri", "tauri-build", "thiserror 2.0.18", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index e03f1f11..110b1f5a 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -27,6 +27,9 @@ tokio-util = "0.7" async-trait = "0.1" uuid = { version = "1", features = ["v4"] } thiserror = "2" +getrandom = "0.3" +base64 = "0.22" +subtle = "2" [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json index 43da9ef6..0eebfc46 100644 --- a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json +++ b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-icon-with-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json index 260dbe05..32866459 100644 --- a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -183,10 +183,10 @@ "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", "type": "string", "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" }, { "description": "Enables the app_hide command without any pre-configured scope.", @@ -260,6 +260,12 @@ "const": "core:app:allow-set-dock-visibility", "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", @@ -344,6 +350,12 @@ "const": "core:app:deny-set-dock-visibility", "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", @@ -867,10 +879,10 @@ "markdownDescription": "Denies the close command without any pre-configured scope." }, { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", "type": "string", "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" }, { "description": "Enables the get_by_id command without any pre-configured scope.", @@ -902,6 +914,12 @@ "const": "core:tray:allow-set-icon-as-template", "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", @@ -968,6 +986,12 @@ "const": "core:tray:deny-set-icon-as-template", "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", @@ -1227,10 +1251,16 @@ "markdownDescription": "Denies the webview_size command without any pre-configured scope." }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", "type": "string", "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." }, { "description": "Enables the available_monitors command without any pre-configured scope.", @@ -1424,6 +1454,12 @@ "const": "core:window:allow-scale-factor", "markdownDescription": "Enables the scale_factor command without any pre-configured scope." }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, { "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", @@ -1688,6 +1724,12 @@ "const": "core:window:allow-unminimize", "markdownDescription": "Enables the unminimize command without any pre-configured scope." }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, { "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", @@ -1880,6 +1922,12 @@ "const": "core:window:deny-scale-factor", "markdownDescription": "Denies the scale_factor command without any pre-configured scope." }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, { "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json index 260dbe05..32866459 100644 --- a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json @@ -183,10 +183,10 @@ "markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`" }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`", "type": "string", "const": "core:app:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`" }, { "description": "Enables the app_hide command without any pre-configured scope.", @@ -260,6 +260,12 @@ "const": "core:app:allow-set-dock-visibility", "markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Enables the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:allow-supports-multiple-windows", + "markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Enables the tauri_version command without any pre-configured scope.", "type": "string", @@ -344,6 +350,12 @@ "const": "core:app:deny-set-dock-visibility", "markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope." }, + { + "description": "Denies the supports_multiple_windows command without any pre-configured scope.", + "type": "string", + "const": "core:app:deny-supports-multiple-windows", + "markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope." + }, { "description": "Denies the tauri_version command without any pre-configured scope.", "type": "string", @@ -867,10 +879,10 @@ "markdownDescription": "Denies the close command without any pre-configured scope." }, { - "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`", + "description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`", "type": "string", "const": "core:tray:default", - "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`" + "markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`" }, { "description": "Enables the get_by_id command without any pre-configured scope.", @@ -902,6 +914,12 @@ "const": "core:tray:allow-set-icon-as-template", "markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Enables the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:allow-set-icon-with-as-template", + "markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Enables the set_menu command without any pre-configured scope.", "type": "string", @@ -968,6 +986,12 @@ "const": "core:tray:deny-set-icon-as-template", "markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope." }, + { + "description": "Denies the set_icon_with_as_template command without any pre-configured scope.", + "type": "string", + "const": "core:tray:deny-set-icon-with-as-template", + "markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope." + }, { "description": "Denies the set_menu command without any pre-configured scope.", "type": "string", @@ -1227,10 +1251,16 @@ "markdownDescription": "Denies the webview_size command without any pre-configured scope." }, { - "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`", + "description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`", "type": "string", "const": "core:window:default", - "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`" + "markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`" + }, + { + "description": "Enables the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-activity-name", + "markdownDescription": "Enables the activity_name command without any pre-configured scope." }, { "description": "Enables the available_monitors command without any pre-configured scope.", @@ -1424,6 +1454,12 @@ "const": "core:window:allow-scale-factor", "markdownDescription": "Enables the scale_factor command without any pre-configured scope." }, + { + "description": "Enables the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:allow-scene-identifier", + "markdownDescription": "Enables the scene_identifier command without any pre-configured scope." + }, { "description": "Enables the set_always_on_bottom command without any pre-configured scope.", "type": "string", @@ -1688,6 +1724,12 @@ "const": "core:window:allow-unminimize", "markdownDescription": "Enables the unminimize command without any pre-configured scope." }, + { + "description": "Denies the activity_name command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-activity-name", + "markdownDescription": "Denies the activity_name command without any pre-configured scope." + }, { "description": "Denies the available_monitors command without any pre-configured scope.", "type": "string", @@ -1880,6 +1922,12 @@ "const": "core:window:deny-scale-factor", "markdownDescription": "Denies the scale_factor command without any pre-configured scope." }, + { + "description": "Denies the scene_identifier command without any pre-configured scope.", + "type": "string", + "const": "core:window:deny-scene-identifier", + "markdownDescription": "Denies the scene_identifier command without any pre-configured scope." + }, { "description": "Denies the set_always_on_bottom command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/src/bridge/mod.rs b/apps/desktop/src-tauri/src/bridge/mod.rs index 254b6e8e..3e0612b8 100644 --- a/apps/desktop/src-tauri/src/bridge/mod.rs +++ b/apps/desktop/src-tauri/src/bridge/mod.rs @@ -3,6 +3,11 @@ //! The extension connects here to: //! 1. Announce discovered browser providers //! 2. Relay SLOP messages for SPA providers (postMessage-based) +//! +//! Loopback is not a trust boundary: every upgrade is gated by Origin +//! validation and pairing-token authentication (see `security`). + +pub mod security; use futures_util::{SinkExt, StreamExt}; use serde_json::Value; @@ -11,11 +16,14 @@ use std::sync::Arc; use tauri::{AppHandle, Emitter, Manager}; use tokio::net::TcpListener; use tokio::sync::{mpsc, Mutex}; -use tokio_tungstenite::accept_async; +use tokio_tungstenite::accept_hdr_async; +use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response}; +use tokio_tungstenite::tungstenite::http::{HeaderValue, StatusCode}; use tokio_tungstenite::tungstenite::Message; use crate::events; use crate::provider::{ProviderRegistry, ProviderSource, ProviderSummary, TransportConfig}; +use security::{BridgeToken, BEARER_PROTOCOL}; const BRIDGE_PORT: u16 = 9339; @@ -74,9 +82,21 @@ pub async fn start_bridge_server(app: AppHandle) { let sinks_clone = sinks.clone(); tokio::spawn(async move { - let ws_stream = match accept_async(stream).await { + // Snapshot the expected token at accept time; connections accepted + // before a regeneration are explicitly closed by `disconnect_all_clients`. + let expected_token = app_clone + .try_state::>() + .map(|t| t.current()) + .unwrap_or_default(); + + let callback = |req: &Request, response: Response| { + gate_upgrade(req, response, &expected_token) + }; + + let ws_stream = match accept_hdr_async(stream, callback).await { Ok(ws) => ws, Err(e) => { + // Note: rejection errors carry only a status code — never the token. eprintln!("Bridge: WebSocket handshake failed: {}", e); return; } @@ -146,6 +166,73 @@ pub async fn start_bridge_server(app: AppHandle) { } } +/// Upgrade gate enforcing the bridge security rules from +/// spec/integrations/desktop.md (Bridge security): +/// +/// 1. Reject web Origins (`http`/`https` scheme, or literal `null`) with 403. +/// Extension origins and absent Origin pass to the token check. +/// 2. Require a valid pairing token offered via +/// `Sec-WebSocket-Protocol: slop.bearer, `; compare in constant +/// time; reject missing/invalid tokens with 401. On success, echo back +/// only the non-secret `slop.bearer` subprotocol — never the token. +fn gate_upgrade( + req: &Request, + mut response: Response, + expected_token: &str, +) -> Result { + if let Some(origin) = req.headers().get("Origin").and_then(|v| v.to_str().ok()) { + if security::origin_is_forbidden(origin) { + return Err(reject(StatusCode::FORBIDDEN)); + } + } + + let protocol_values: Vec<&str> = req + .headers() + .get_all("Sec-WebSocket-Protocol") + .iter() + .filter_map(|v| v.to_str().ok()) + .collect(); + + match security::extract_bearer_token(&protocol_values) { + Some(offered) if security::token_matches(&offered, expected_token) => { + response.headers_mut().insert( + "Sec-WebSocket-Protocol", + HeaderValue::from_static(BEARER_PROTOCOL), + ); + Ok(response) + } + _ => Err(reject(StatusCode::UNAUTHORIZED)), + } +} + +fn reject(status: StatusCode) -> ErrorResponse { + let mut response = ErrorResponse::new(None); + *response.status_mut() = status; + response +} + +/// Close every connected bridge client. Used when the pairing token is +/// regenerated so stale peers must re-pair with the new token. +pub async fn disconnect_all_clients(app: &AppHandle) { + let Some(sinks_state) = app.try_state::() else { + return; + }; + + let drained: Vec<_> = { + let mut sinks = sinks_state.0.lock().await; + sinks.drain(..).collect() + }; + + for sink in drained { + let mut s = sink.lock().await; + let _ = s.send(Message::Close(None)).await; + let _ = s.close().await; + } + + clear_all_relays(app).await; + let _ = app.emit("bridge-status", false); +} + async fn handle_bridge_message(app: &AppHandle, value: &Value) { let msg_type = value["type"].as_str().unwrap_or(""); @@ -269,3 +356,84 @@ pub async fn bridge_send_value(app: AppHandle, message: Value) -> Result<(), Str Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + const TOKEN: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEF_"; + + fn upgrade_request(origin: Option<&str>, protocols: Option<&str>) -> Request { + let mut builder = Request::builder().uri("ws://127.0.0.1:9339/slop-bridge"); + if let Some(origin) = origin { + builder = builder.header("Origin", origin); + } + if let Some(protocols) = protocols { + builder = builder.header("Sec-WebSocket-Protocol", protocols); + } + builder.body(()).unwrap() + } + + fn gate(origin: Option<&str>, protocols: Option<&str>) -> Result { + let req = upgrade_request(origin, protocols); + let response = Response::new(()); + gate_upgrade(&req, response, TOKEN) + } + + #[test] + fn rejects_web_origin_with_403_even_with_valid_token() { + for origin in ["http://localhost:3000", "https://evil.example.com", "null"] { + let err = gate(Some(origin), Some(&format!("slop.bearer, {}", TOKEN))) + .expect_err("web origin must be rejected"); + assert_eq!(err.status(), StatusCode::FORBIDDEN); + } + } + + #[test] + fn accepts_extension_origin_with_valid_token_and_echoes_label_only() { + let response = gate( + Some("chrome-extension://abcdefghijklmnop"), + Some(&format!("slop.bearer, {}", TOKEN)), + ) + .expect("extension origin with valid token must be accepted"); + + let echoed = response + .headers() + .get("Sec-WebSocket-Protocol") + .and_then(|v| v.to_str().ok()) + .expect("subprotocol must be echoed"); + assert_eq!(echoed, BEARER_PROTOCOL); + assert!(!echoed.contains(TOKEN)); + } + + #[test] + fn accepts_absent_origin_with_valid_token() { + let response = gate(None, Some(&format!("slop.bearer, {}", TOKEN))) + .expect("native client with valid token must be accepted"); + assert_eq!( + response + .headers() + .get("Sec-WebSocket-Protocol") + .and_then(|v| v.to_str().ok()), + Some(BEARER_PROTOCOL) + ); + } + + #[test] + fn rejects_missing_token_with_401() { + for protocols in [None, Some("slop.bearer"), Some("some-other-protocol")] { + let err = gate(None, protocols).expect_err("missing token must be rejected"); + assert_eq!(err.status(), StatusCode::UNAUTHORIZED); + } + } + + #[test] + fn rejects_invalid_token_with_401() { + let err = gate( + Some("moz-extension://uuid-here"), + Some("slop.bearer, wrong-token"), + ) + .expect_err("invalid token must be rejected"); + assert_eq!(err.status(), StatusCode::UNAUTHORIZED); + } +} diff --git a/apps/desktop/src-tauri/src/bridge/security.rs b/apps/desktop/src-tauri/src/bridge/security.rs new file mode 100644 index 00000000..8f5a2dac --- /dev/null +++ b/apps/desktop/src-tauri/src/bridge/security.rs @@ -0,0 +1,286 @@ +//! Bridge upgrade security: Origin validation and pairing-token authentication. +//! +//! Implements the `Sec-WebSocket-Protocol` bearer-token pattern from +//! spec/core/transport.md (Security considerations) specialized for the +//! bridge's one legitimate peer — the browser extension: +//! +//! - Upgrades carrying a web `Origin` (`http`/`https` scheme, or the literal +//! `null`) are rejected with 403. Extension origins and absent Origin pass. +//! - The client offers `Sec-WebSocket-Protocol: slop.bearer, `. The +//! server verifies the token in constant time and echoes back only the +//! non-secret `slop.bearer` label. Missing/invalid token is rejected with 401. +//! - The token is never logged. + +use std::path::PathBuf; +use std::sync::RwLock; + +use base64::Engine; +use serde::{Deserialize, Serialize}; +use subtle::ConstantTimeEq; + +/// Subprotocol label offered alongside the token; the only subprotocol the +/// server ever echoes back on a successful upgrade. +pub const BEARER_PROTOCOL: &str = "slop.bearer"; + +const TOKEN_FILE: &str = "bridge-pairing.json"; + +/// Returns `true` when an `Origin` header value identifies a web page (scheme +/// `http` or `https`) or is the opaque literal `null` — such upgrades must be +/// rejected with 403. Extension-scheme origins (`chrome-extension://...`, +/// `moz-extension://...`, `safari-web-extension://...`) pass this check; the +/// token check still applies to them. +pub fn origin_is_forbidden(origin: &str) -> bool { + let origin = origin.trim(); + if origin.eq_ignore_ascii_case("null") { + return true; + } + let scheme = origin.split(':').next().unwrap_or(""); + scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https") +} + +/// Extracts the bearer token from `Sec-WebSocket-Protocol` header values. +/// +/// The client offers two subprotocol entries — the literal label +/// `slop.bearer` and the token value — either as one comma-separated header +/// (`Sec-WebSocket-Protocol: slop.bearer, `) or as repeated headers. +/// Returns the token entry only when the `slop.bearer` label is also present. +pub fn extract_bearer_token(header_values: &[&str]) -> Option { + let mut label_seen = false; + let mut token: Option = None; + + for value in header_values { + for entry in value.split(',') { + let entry = entry.trim(); + if entry.is_empty() { + continue; + } + if entry == BEARER_PROTOCOL { + label_seen = true; + } else if token.is_none() { + token = Some(entry.to_string()); + } + } + } + + if label_seen { + token + } else { + None + } +} + +/// Constant-time comparison of the offered token against the expected one. +/// Never matches an empty expected token (a misconfigured store must not +/// become an open door). +pub fn token_matches(offered: &str, expected: &str) -> bool { + !expected.is_empty() && bool::from(offered.as_bytes().ct_eq(expected.as_bytes())) +} + +/// Generates a 32-byte cryptographically random token, base64url-encoded +/// without padding. +pub fn generate_token() -> Result { + let mut bytes = [0u8; 32]; + getrandom::fill(&mut bytes).map_err(|e| format!("failed to gather entropy: {}", e))?; + Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes)) +} + +#[derive(Serialize, Deserialize)] +struct TokenStorage { + token: String, +} + +/// Bridge pairing token, persisted in the app data directory alongside the +/// other settings files (`bridge-pairing.json`). +pub struct BridgeToken { + token: RwLock, + path: PathBuf, +} + +impl BridgeToken { + /// Loads the persisted pairing token, generating and persisting a fresh + /// one on first launch (or if the stored file is missing/corrupt). + pub fn load_or_create(app_data_dir: PathBuf) -> Self { + let path = app_data_dir.join(TOKEN_FILE); + + let existing = std::fs::read_to_string(&path) + .ok() + .and_then(|content| serde_json::from_str::(&content).ok()) + .map(|storage| storage.token) + .filter(|token| !token.is_empty()); + + let token = match existing { + Some(token) => token, + None => { + let token = generate_token().unwrap_or_default(); + if !token.is_empty() { + save_token(&path, &token); + } + token + } + }; + + Self { + token: RwLock::new(token), + path, + } + } + + /// Current pairing token. + pub fn current(&self) -> String { + self.token.read().map(|t| t.clone()).unwrap_or_default() + } + + /// Generates, persists, and swaps in a new pairing token. Existing bridge + /// connections must be disconnected by the caller. + pub fn regenerate(&self) -> Result { + let new_token = generate_token()?; + save_token(&self.path, &new_token); + if let Ok(mut guard) = self.token.write() { + *guard = new_token.clone(); + } + Ok(new_token) + } +} + +fn save_token(path: &PathBuf, token: &str) { + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let storage = TokenStorage { + token: token.to_string(), + }; + if let Ok(json) = serde_json::to_string_pretty(&storage) { + let _ = std::fs::write(path, json); + // The token file is a credential: restrict to the owner. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── Origin gate ───────────────────────────────────────────────────── + + #[test] + fn rejects_http_and_https_origins() { + assert!(origin_is_forbidden("http://localhost:3000")); + assert!(origin_is_forbidden("https://evil.example.com")); + assert!(origin_is_forbidden("HTTPS://EVIL.EXAMPLE.COM")); + assert!(origin_is_forbidden(" http://padded.example ")); + } + + #[test] + fn rejects_null_origin() { + assert!(origin_is_forbidden("null")); + assert!(origin_is_forbidden("NULL")); + } + + #[test] + fn allows_extension_origins() { + assert!(!origin_is_forbidden("chrome-extension://abcdefghijklmnop")); + assert!(!origin_is_forbidden("moz-extension://uuid-here")); + assert!(!origin_is_forbidden("safari-web-extension://uuid-here")); + } + + // ── Subprotocol token extraction ──────────────────────────────────── + + #[test] + fn extracts_token_from_single_comma_separated_header() { + let values = ["slop.bearer, my-secret-token"]; + assert_eq!( + extract_bearer_token(&values), + Some("my-secret-token".to_string()) + ); + } + + #[test] + fn extracts_token_from_repeated_headers() { + let values = ["slop.bearer", "my-secret-token"]; + assert_eq!( + extract_bearer_token(&values), + Some("my-secret-token".to_string()) + ); + } + + #[test] + fn extracts_token_regardless_of_entry_order() { + let values = ["my-secret-token, slop.bearer"]; + assert_eq!( + extract_bearer_token(&values), + Some("my-secret-token".to_string()) + ); + } + + #[test] + fn no_token_without_bearer_label() { + let values = ["my-secret-token"]; + assert_eq!(extract_bearer_token(&values), None); + } + + #[test] + fn no_token_with_label_only() { + let values = ["slop.bearer"]; + assert_eq!(extract_bearer_token(&values), None); + assert_eq!(extract_bearer_token(&[]), None); + } + + // ── Token comparison ──────────────────────────────────────────────── + + #[test] + fn token_matches_exact_value_only() { + assert!(token_matches("abc123", "abc123")); + assert!(!token_matches("abc124", "abc123")); + assert!(!token_matches("abc1234", "abc123")); + assert!(!token_matches("", "abc123")); + } + + #[test] + fn empty_expected_token_never_matches() { + assert!(!token_matches("", "")); + assert!(!token_matches("anything", "")); + } + + // ── Token generation & persistence ────────────────────────────────── + + #[test] + fn generated_token_is_base64url_no_padding() { + let token = generate_token().expect("token generation"); + // 32 bytes → 43 base64url chars, no '=' padding. + assert_eq!(token.len(), 43); + assert!(token + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')); + } + + #[test] + fn generated_tokens_are_unique() { + assert_ne!(generate_token().unwrap(), generate_token().unwrap()); + } + + #[test] + fn token_persists_across_loads_and_regenerates() { + let dir = std::env::temp_dir().join(format!("slop-bridge-token-test-{}", uuid::Uuid::new_v4())); + + let first = BridgeToken::load_or_create(dir.clone()); + let token_a = first.current(); + assert_eq!(token_a.len(), 43); + + // Second load reads the same persisted token. + let second = BridgeToken::load_or_create(dir.clone()); + assert_eq!(second.current(), token_a); + + // Regeneration produces and persists a different token. + let token_b = second.regenerate().expect("regenerate"); + assert_ne!(token_b, token_a); + assert_eq!(second.current(), token_b); + let third = BridgeToken::load_or_create(dir.clone()); + assert_eq!(third.current(), token_b); + + let _ = std::fs::remove_dir_all(&dir); + } +} diff --git a/apps/desktop/src-tauri/src/commands.rs b/apps/desktop/src-tauri/src/commands.rs index 80c5c439..ebb35982 100644 --- a/apps/desktop/src-tauri/src/commands.rs +++ b/apps/desktop/src-tauri/src/commands.rs @@ -411,3 +411,21 @@ pub async fn fetch_models( pub async fn bridge_send(app: AppHandle, message: Value) -> Result<(), String> { bridge::bridge_send_value(app, message).await } + +#[tauri::command] +pub async fn get_bridge_pairing_token( + token: State<'_, Arc>, +) -> Result { + Ok(token.current()) +} + +#[tauri::command] +pub async fn regenerate_bridge_pairing_token( + app: AppHandle, + token: State<'_, Arc>, +) -> Result { + let new_token = token.regenerate()?; + // Stale peers paired with the old token must reconnect with the new one. + bridge::disconnect_all_clients(&app).await; + Ok(new_token) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index f9794374..7787f5ea 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -23,10 +23,14 @@ pub fn run() { let workspace_mgr = workspace::WorkspaceManager::new(app_data_dir.clone()); let provider_registry = provider::ProviderRegistry::new(); let profile_mgr = llm::profiles::ProfileManager::new(app_data_dir.clone()); + let bridge_token = std::sync::Arc::new( + bridge::security::BridgeToken::load_or_create(app_data_dir.clone()), + ); app.manage(workspace_mgr.clone()); app.manage(provider_registry.clone()); app.manage(profile_mgr); + app.manage(bridge_token); // Initial provider discovery { @@ -87,6 +91,8 @@ pub fn run() { commands::fetch_models, // Bridge commands::bridge_send, + commands::get_bridge_pairing_token, + commands::regenerate_bridge_pairing_token, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/apps/desktop/src/App.css b/apps/desktop/src/App.css index bb4bdb03..f6911c0c 100644 --- a/apps/desktop/src/App.css +++ b/apps/desktop/src/App.css @@ -699,6 +699,81 @@ body { color: #adc6ff; } +.settings-section-label { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: #8b949e; + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 16px 0 8px; +} + +.settings-section-label:first-child { + margin-top: 0; +} + +.bridge-pairing { + padding: 12px; + border-radius: 4px; + background: #0c0e14; +} + +.bridge-pairing-hint { + font-size: 11px; + color: #8b949e; + margin: 0 0 10px; +} + +.bridge-token-row { + display: flex; + gap: 8px; +} + +.bridge-token-row input { + flex: 1; + background: #232733; + border: none; + border-bottom: 2px solid transparent; + color: #e6edf3; + padding: 8px 10px; + border-radius: 4px; + font-size: 12px; + font-family: 'JetBrains Mono', monospace; + outline: none; +} + +.bridge-token-row input:focus { + border-bottom: 2px solid #91db37; +} + +.bridge-token-row button, +.bridge-pairing-actions button { + padding: 6px 14px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-family: 'JetBrains Mono', monospace; + text-transform: uppercase; + letter-spacing: 0.05em; + border: 1px solid rgba(173, 198, 255, 0.1); +} + +.bridge-token-row button:disabled { + opacity: 0.5; + cursor: default; +} + +.bridge-pairing-actions { + margin-top: 10px; + display: flex; + align-items: center; + gap: 10px; +} + +.bridge-pairing-actions .bridge-pairing-hint { + margin: 0; +} + /* ─────────────────────────────────────────────── Sidebar Groups ─────────────────────────────────────────────── */ diff --git a/apps/desktop/src/components/Settings.tsx b/apps/desktop/src/components/Settings.tsx index e7989623..76eac01b 100644 --- a/apps/desktop/src/components/Settings.tsx +++ b/apps/desktop/src/components/Settings.tsx @@ -1,6 +1,7 @@ -import { useState } from "react"; -import { useAppStore } from "../stores/app-store"; +import { useEffect, useRef, useState } from "react"; +import { getBridgePairingToken, regenerateBridgePairingToken } from "../lib/commands"; import type { LlmProfile } from "../lib/types"; +import { useAppStore } from "../stores/app-store"; interface SettingsProps { onClose: () => void; @@ -31,6 +32,39 @@ export function Settings({ onClose }: SettingsProps) { const [editing, setEditing] = useState(null); const [isNew, setIsNew] = useState(false); + const [bridgeToken, setBridgeToken] = useState(""); + const [tokenCopied, setTokenCopied] = useState(false); + const tokenInputRef = useRef(null); + + useEffect(() => { + getBridgePairingToken() + .then(setBridgeToken) + .catch(() => setBridgeToken("")); + }, []); + + async function handleCopyToken() { + if (!bridgeToken) return; + try { + await navigator.clipboard.writeText(bridgeToken); + } catch { + // Clipboard API unavailable — select the token so the user can copy manually + tokenInputRef.current?.select(); + return; + } + setTokenCopied(true); + setTimeout(() => setTokenCopied(false), 1500); + } + + async function handleRegenerateToken() { + try { + const next = await regenerateBridgePairingToken(); + setBridgeToken(next); + setTokenCopied(false); + } catch { + // Keep the current token on failure + } + } + function startNew() { setIsNew(true); setEditing({ @@ -76,10 +110,11 @@ export function Settings({ onClose }: SettingsProps) {
e.stopPropagation()}>
- LLM Profiles + Settings
+
LLM Profiles
@@ -158,6 +193,32 @@ export function Settings({ onClose }: SettingsProps) {
)} + +
Extension Bridge
+
+

+ Paste this pairing token into the SLOP browser extension to authorize it to connect to the desktop bridge. +

+
+ e.currentTarget.select()} + /> + +
+
+ + Regenerating disconnects the extension until it re-pairs. +
+
diff --git a/apps/desktop/src/lib/commands.ts b/apps/desktop/src/lib/commands.ts index bccc177d..ece192c1 100644 --- a/apps/desktop/src/lib/commands.ts +++ b/apps/desktop/src/lib/commands.ts @@ -1,5 +1,5 @@ import { invoke } from "@tauri-apps/api/core"; -import type { WorkspaceSummary, WorkspaceDetail, ProviderSummary, ProviderConnectResult, LlmProfile } from "./types"; +import type { LlmProfile, ProviderConnectResult, ProviderSummary, WorkspaceDetail, WorkspaceSummary } from "./types"; // Workspace commands export const listWorkspaces = () => invoke("list_workspaces"); @@ -38,3 +38,5 @@ export const fetchModels = () => invoke("fetch_models"); // Bridge commands export const bridgeSend = (message: unknown) => invoke("bridge_send", { message }); +export const getBridgePairingToken = () => invoke("get_bridge_pairing_token"); +export const regenerateBridgePairingToken = () => invoke("regenerate_bridge_pairing_token"); diff --git a/apps/extension/__tests__/bridge-auth.test.ts b/apps/extension/__tests__/bridge-auth.test.ts new file mode 100644 index 00000000..87d8a59c --- /dev/null +++ b/apps/extension/__tests__/bridge-auth.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from "bun:test"; +import { BRIDGE_BEARER_PROTOCOL, buildBridgeProtocols, isAcceptedBridgeProtocol } from "../src/lib/bridge-auth"; + +describe("buildBridgeProtocols", () => { + test("offers the literal label plus the token", () => { + expect(buildBridgeProtocols("s3cret-token")).toEqual(["slop.bearer", "s3cret-token"]); + }); + + test("label constant matches the spec", () => { + expect(BRIDGE_BEARER_PROTOCOL).toBe("slop.bearer"); + }); +}); + +describe("isAcceptedBridgeProtocol", () => { + test("accepts only the non-secret label echoed by the server", () => { + expect(isAcceptedBridgeProtocol("slop.bearer")).toBe(true); + }); + + test("rejects anything else (including a token echo or empty selection)", () => { + expect(isAcceptedBridgeProtocol("")).toBe(false); + expect(isAcceptedBridgeProtocol("s3cret-token")).toBe(false); + expect(isAcceptedBridgeProtocol("slop.bearer, s3cret-token")).toBe(false); + }); +}); diff --git a/apps/extension/__tests__/origin.test.ts b/apps/extension/__tests__/origin.test.ts new file mode 100644 index 00000000..1d8b98d3 --- /dev/null +++ b/apps/extension/__tests__/origin.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, test } from "bun:test"; +import { approvalKey, classifyDiscoveryTarget, isLoopbackHostname, pageOriginOf } from "../src/lib/origin"; + +describe("isLoopbackHostname", () => { + test("recognizes the loopback family", () => { + expect(isLoopbackHostname("localhost")).toBe(true); + expect(isLoopbackHostname("LOCALHOST")).toBe(true); + expect(isLoopbackHostname("app.localhost")).toBe(true); + expect(isLoopbackHostname("127.0.0.1")).toBe(true); + expect(isLoopbackHostname("127.1.2.3")).toBe(true); + expect(isLoopbackHostname("::1")).toBe(true); + expect(isLoopbackHostname("[::1]")).toBe(true); + }); + + test("rejects non-loopback hosts", () => { + expect(isLoopbackHostname("example.com")).toBe(false); + expect(isLoopbackHostname("localhost.example.com")).toBe(false); + expect(isLoopbackHostname("128.0.0.1")).toBe(false); + expect(isLoopbackHostname("::2")).toBe(false); + }); +}); + +describe("classifyDiscoveryTarget", () => { + test("same host is same-origin (port ignored)", () => { + expect(classifyDiscoveryTarget("https://app.example.com/page", "wss://app.example.com/slop")).toBe("same-origin"); + expect(classifyDiscoveryTarget("https://app.example.com/page", "wss://app.example.com:9000/slop")).toBe( + "same-origin", + ); + }); + + test("hostname comparison is case-insensitive", () => { + expect(classifyDiscoveryTarget("https://App.Example.com/", "wss://app.example.COM/slop")).toBe("same-origin"); + }); + + test("different host is cross-origin", () => { + expect(classifyDiscoveryTarget("https://evil.example/", "wss://app.example.com/slop")).toBe("cross-origin"); + expect(classifyDiscoveryTarget("https://app.example.com/", "wss://api.example.com/slop")).toBe("cross-origin"); + }); + + test("loopback page declaring a loopback target is same-origin (canonical dev case)", () => { + expect(classifyDiscoveryTarget("http://localhost:3000/", "ws://localhost:3737/slop")).toBe("same-origin"); + // The loopback family counts as one host + expect(classifyDiscoveryTarget("http://localhost:3000/", "ws://127.0.0.1:3737/slop")).toBe("same-origin"); + expect(classifyDiscoveryTarget("http://127.0.0.1:3000/", "ws://[::1]:3737/slop")).toBe("same-origin"); + }); + + test("loopback target declared by a non-loopback page is cross-origin", () => { + expect(classifyDiscoveryTarget("https://evil.example/", "ws://127.0.0.1:9339/slop-bridge")).toBe("cross-origin"); + expect(classifyDiscoveryTarget("https://evil.example/", "ws://localhost:3737/slop")).toBe("cross-origin"); + expect(classifyDiscoveryTarget("https://evil.example/", "ws://[::1]:9339/slop")).toBe("cross-origin"); + }); + + test("loopback page declaring a remote target is cross-origin", () => { + expect(classifyDiscoveryTarget("http://localhost:3000/", "wss://api.example.com/slop")).toBe("cross-origin"); + }); + + test("unparseable inputs are cross-origin (untrusted by default)", () => { + expect(classifyDiscoveryTarget("not a url", "ws://localhost:3737/slop")).toBe("cross-origin"); + expect(classifyDiscoveryTarget("https://app.example.com/", "not a url")).toBe("cross-origin"); + expect(classifyDiscoveryTarget("", "")).toBe("cross-origin"); + }); +}); + +describe("pageOriginOf", () => { + test("returns scheme://host[:port]", () => { + expect(pageOriginOf("https://app.example.com:8443/deep/path?q=1")).toBe("https://app.example.com:8443"); + expect(pageOriginOf("http://localhost:3000/")).toBe("http://localhost:3000"); + }); + + test("returns null for unparseable or opaque origins", () => { + expect(pageOriginOf("not a url")).toBeNull(); + expect(pageOriginOf("")).toBeNull(); + }); +}); + +describe("approvalKey", () => { + test("is stable per (page origin, target URL) pair", () => { + const a = approvalKey("https://app.example.com", "ws://other.example.com/slop"); + const b = approvalKey("https://app.example.com", "ws://other.example.com/slop"); + const c = approvalKey("https://elsewhere.example.com", "ws://other.example.com/slop"); + expect(a).toBe(b); + expect(a).not.toBe(c); + }); +}); diff --git a/apps/extension/package.json b/apps/extension/package.json index c77caf4c..3372695c 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -6,7 +6,8 @@ "scripts": { "build": "bun run build.ts", "dev": "bun run build.ts --watch", - "typecheck": "bunx tsc --noEmit -p tsconfig.json" + "typecheck": "bunx tsc --noEmit -p tsconfig.json", + "test": "bun test" }, "dependencies": { "@slop-ai/consumer": "workspace:*" diff --git a/apps/extension/popup.html b/apps/extension/popup.html index 88643a02..20c7893c 100644 --- a/apps/extension/popup.html +++ b/apps/extension/popup.html @@ -89,6 +89,59 @@ .dot.green { background: #91db37; } .dot.yellow { background: #d29922; } .dot.gray { background: #6e7681; } + .dot.red { background: #f85149; } + + .providers-section { + margin-bottom: 12px; padding: 10px 12px; + background: #1a1d27; border-radius: 4px; + } + .providers-title { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; color: #6e7681; + text-transform: uppercase; letter-spacing: 0.05em; + margin-bottom: 8px; + } + .provider-item { padding: 6px 0; } + .provider-item + .provider-item { border-top: 1px solid rgba(173, 198, 255, 0.1); } + .provider-name { + font-size: 13px; font-weight: 600; color: #e6edf3; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .provider-sub { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; color: #6e7681; margin-top: 2px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .provider-warn { font-size: 11px; color: #d29922; margin-top: 4px; } + .connect-btn { + margin-top: 6px; width: 100%; padding: 6px; border: none; border-radius: 4px; + font-family: 'JetBrains Mono', monospace; + font-size: 12px; font-weight: 500; cursor: pointer; + background: linear-gradient(135deg, #91db37, #6ba62a); color: #111319; + transition: box-shadow 0.2s; + } + .connect-btn:hover { box-shadow: 0 0 15px rgba(145, 219, 55, 0.4); } + + .bridge-section { padding: 8px 0 4px; } + .bridge-status { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; color: #8b949e; margin-bottom: 8px; + } + .pair-row { display: flex; gap: 6px; } + .pair-row input { + flex: 1; min-width: 0; padding: 7px 8px; + background: #111319; border: 1px solid rgba(173, 198, 255, 0.15); border-radius: 4px; + color: #e6edf3; font-family: 'JetBrains Mono', monospace; font-size: 12px; + } + .pair-row input:focus { outline: none; border-color: #91db37; } + .pair-row button { + padding: 7px 12px; border: none; border-radius: 4px; cursor: pointer; + font-family: 'JetBrains Mono', monospace; font-size: 12px; font-weight: 500; + background: linear-gradient(135deg, #91db37, #6ba62a); color: #111319; + transition: box-shadow 0.2s; + } + .pair-row button:hover { box-shadow: 0 0 15px rgba(145, 219, 55, 0.4); } + .pair-hint { font-size: 11px; color: #6e7681; margin-top: 6px; } .scan-btn { width: 100%; padding: 8px; border: none; border-radius: 4px; font-family: 'JetBrains Mono', monospace; @@ -126,6 +179,11 @@

SLOP

+ +
@@ -148,6 +206,15 @@

SLOP

+ +