@frockbot/plugin-settings 0.3.9 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-settings",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,15 +21,15 @@
21
21
  "typecheck": "vue-tsc --noEmit -p tsconfig.json"
22
22
  },
23
23
  "dependencies": {
24
- "@frockbot/catalog-core": "0.3.9",
25
- "@frockbot/client-core": "0.3.9",
26
- "@frockbot/client-ui": "0.3.9",
27
- "@frockbot/configuration-core": "0.3.9",
28
- "@frockbot/connection-core": "0.3.9",
29
- "@frockbot/kernel-composition": "0.3.9",
30
- "@frockbot/kernel-contracts": "0.3.9",
31
- "@frockbot/plugin-auth": "0.3.9",
32
- "@frockbot/plugin-shell": "0.3.9",
24
+ "@frockbot/catalog-core": "0.3.10",
25
+ "@frockbot/client-core": "0.3.10",
26
+ "@frockbot/client-ui": "0.3.10",
27
+ "@frockbot/configuration-core": "0.3.10",
28
+ "@frockbot/connection-core": "0.3.10",
29
+ "@frockbot/kernel-composition": "0.3.10",
30
+ "@frockbot/kernel-contracts": "0.3.10",
31
+ "@frockbot/plugin-auth": "0.3.10",
32
+ "@frockbot/plugin-shell": "0.3.10",
33
33
  "cordis": "4.0.0-rc.8",
34
34
  "vue": "3.5.41"
35
35
  },
package/src/user.test.ts CHANGED
@@ -746,6 +746,51 @@ describe("User settings backend Contribution", () => {
746
746
  },
747
747
  }),
748
748
  ).resolves.toMatchObject({ status: "applied", revision: 4 });
749
+
750
+ // The mirror of the rule above, read the other way round. Disabling the
751
+ // dependency is allowed — refusing it would make a Package the User never
752
+ // chose able to pin one they did — and carries its dependents with it, so
753
+ // the account never lands in the state the enable path refuses:
754
+ // `custom-models=disabled provider-ollama-cloud=installed`.
755
+ await expect(
756
+ settings.executeConfiguration({
757
+ schemaVersion: 1,
758
+ userId: "user-1",
759
+ command: {
760
+ schemaVersion: 1,
761
+ type: "user/set-package-enabled",
762
+ commandId: "disable-custom-models",
763
+ expectedRevision: 4,
764
+ packageId: "custom-models",
765
+ enabled: false,
766
+ },
767
+ }),
768
+ ).resolves.toMatchObject({ status: "applied", revision: 5 });
769
+ const cascaded = await settings.read("user-1");
770
+ expect(cascaded.packages[0]).toMatchObject({
771
+ packageId: "custom-models",
772
+ state: "disabled",
773
+ });
774
+ expect(cascaded.packages[1]).toMatchObject({
775
+ packageId: "provider-ollama-cloud",
776
+ state: "disabled",
777
+ });
778
+
779
+ // The cascade is not automatic re-enablement: turning the dependency back
780
+ // on leaves the dependent off until the User asks for it.
781
+ await settings.executeConfiguration({
782
+ schemaVersion: 1,
783
+ userId: "user-1",
784
+ command: {
785
+ schemaVersion: 1,
786
+ type: "user/set-package-enabled",
787
+ commandId: "re-enable-custom-models",
788
+ expectedRevision: 5,
789
+ packageId: "custom-models",
790
+ enabled: true,
791
+ },
792
+ });
793
+ expect((await settings.read("user-1")).packages[1]?.state).toBe("disabled");
749
794
  });
750
795
 
751
796
  test("applies the platform model through the backend Contribution", async () => {
package/src/user.ts CHANGED
@@ -731,6 +731,54 @@ export class UserSettingsBackendContribution {
731
731
  return undefined;
732
732
  }
733
733
 
734
+ /**
735
+ * The mirror of `packageDependencyFailure`, read the other way round.
736
+ *
737
+ * Enabling B before A is refused, so leaving B enabled once A is switched
738
+ * off would put the account in exactly the configuration the enable path
739
+ * will not create. Refusing the disable is not the answer either: a Package
740
+ * the User never chose — `provider-ollama-cloud` depends on `web` — would
741
+ * make `web` undisableable. So the disable is allowed and carries its
742
+ * dependents with it, transitively. Model binding degrades to the platform
743
+ * default rather than failing, so a cascade that reaches the Package a Bot's
744
+ * model is bound to costs that Bot its choice, not its next reply.
745
+ *
746
+ * Platform-owned Packages are never cascaded: they cannot be disabled by any
747
+ * other path, and in this application they depend only on each other.
748
+ */
749
+ private cascadeDisabledDependents(
750
+ settings: UserSettingsViewV1,
751
+ ): UserSettingsViewV1 {
752
+ let packages = settings.packages;
753
+ for (;;) {
754
+ const enabled = new Set(
755
+ packages
756
+ .filter((pkg) => pkg.state === "installed")
757
+ .map((pkg) => pkg.packageId),
758
+ );
759
+ const cascaded = packages.map((pkg) => {
760
+ if (pkg.state !== "installed") return pkg;
761
+ if (this.platformOwnedPackageIds.has(pkg.packageId)) return pkg;
762
+ const dependencies = this.packageDependencies.get(
763
+ `${pkg.packageId}\u0000${pkg.version}`,
764
+ );
765
+ if (!dependencies) return pkg;
766
+ const missing = Object.keys(dependencies).some(
767
+ (dependencyId) => !enabled.has(dependencyId),
768
+ );
769
+ return missing
770
+ ? { ...pkg, state: "disabled" as const, failure: undefined }
771
+ : pkg;
772
+ });
773
+ if (cascaded.every((pkg, index) => pkg === packages[index])) {
774
+ return packages === settings.packages
775
+ ? settings
776
+ : { ...settings, packages };
777
+ }
778
+ packages = cascaded;
779
+ }
780
+ }
781
+
734
782
  async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
735
783
  const request = decodeUserConfigurationReadRpcV1(input);
736
784
  // Settings owns the stored-record migration and platform-row repair. Run
@@ -1001,9 +1049,16 @@ export class UserSettingsBackendContribution {
1001
1049
  await storage.put(receiptKey, { commandFingerprint, receipt });
1002
1050
  return receipt;
1003
1051
  }
1004
- const next = applyUserCommand(current, command, (packageId, version) =>
1052
+ const applied = applyUserCommand(current, command, (packageId, version) =>
1005
1053
  this.settingDefinitions(packageId, version),
1006
1054
  );
1055
+ // One command, one revision: the cascade is part of the disable the User
1056
+ // asked for, not a second write they have to reconcile against.
1057
+ const next =
1058
+ command.type === "user/uninstall-package" ||
1059
+ (command.type === "user/set-package-enabled" && !command.enabled)
1060
+ ? this.cascadeDisabledDependents(applied)
1061
+ : applied;
1007
1062
  const receipt: OperationReceiptV1 = {
1008
1063
  schemaVersion: 1,
1009
1064
  commandId: command.commandId,