Skip to content

Commit 6bb1460

Browse files
authored
Merge pull request #1649 from jaylfc/dev
release: promote dev to master (v1.0.0-beta.28)
2 parents d99a603 + 0ac11c1 commit 6bb1460

42 files changed

Lines changed: 2274 additions & 449 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ Versions follow semver beta: `1.0.0-beta.N`, bumped on each dev->master promotio
77

88
## [Unreleased]
99

10+
## [1.0.0-beta.28] - 2026-07-05
11+
12+
### Added
13+
- App Studio is now real: describe an app in plain words and the taOS agent generates it, packages it, runs it through the security analyzer, installs it, and shows it running live in a sandboxed window, all in one flow. Generated apps ship as sandboxed web apps with no elevated permissions.
14+
- Licensing transparency: services whose model weights are non-commercial (MusicGen, MusicGPT, FLUX-Fill) now carry accurate weight-license metadata (the code license was already MIT, but the weights are CC-BY-NC), the Store shows a "Non-commercial weights" badge, and installing such a service now requires a one-time license acceptance. Nothing non-commercial installs silently.
15+
16+
### Fixed
17+
- Video Studio generation is no longer a multi-minute blocking request that could time out or fail on a disconnect. Generation now runs as a background job: you get an immediate job id, the UI polls for progress, and a failed job always ends in a clear error state instead of hanging.
18+
1019
## [1.0.0-beta.27] - 2026-07-04
1120

1221
### Added

app-catalog/services/flux-fill/manifest.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ version: 1.0.0
66
description: "FLUX.1-Fill quality inpaint/outpaint tier - GPU image editing via an A1111-compatible sd.cpp server. Runs on-demand on a shared NVIDIA GPU."
77
homepage: https://github.com/leejet/stable-diffusion.cpp
88
license: MIT
9+
# The sd.cpp server is MIT, but this tier pulls the FLUX.1-dev weights,
10+
# which ship under the FLUX.1-dev non-commercial license.
11+
weights_license: "flux-1-dev-non-commercial-license"
12+
license_class: non-commercial
913

1014
requires:
1115
ram_mb: 8192

app-catalog/services/musicgen/manifest.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ version: 1.3.0
66
description: "Meta's text-to-music — generates 30s clips from prompts. Multiple model sizes (small/medium/large/melody). Conditioning on melody supported."
77
homepage: https://github.com/facebookresearch/audiocraft
88
license: MIT
9+
# The wrapper code is MIT, but the pretrained MusicGen weights this service
10+
# downloads are Meta's CC-BY-NC 4.0 -- non-commercial use only.
11+
weights_license: "CC-BY-NC 4.0"
12+
license_class: non-commercial
913

1014
requires:
1115
ram_mb: 8192

app-catalog/services/musicgpt/manifest.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ version: 0.4.0
66
description: "Generate music from text prompts — runs locally using Meta's MusicGen, no Python required"
77
homepage: https://github.com/gabotechs/MusicGPT
88
license: MIT
9+
# The server binary is MIT, but it runs Meta's MusicGen weights under the
10+
# hood, which are CC-BY-NC 4.0 -- non-commercial use only.
11+
weights_license: "CC-BY-NC 4.0"
12+
license_class: non-commercial
913

1014
requires:
1115
ram_mb: 2048

desktop/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "tinyagentos-desktop",
33
"private": true,
4-
"version": "1.0.0-beta.27",
4+
"version": "1.0.0-beta.28",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

desktop/src/apps/AppStudioApp.test.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,13 @@ describe("AppStudioApp", () => {
4040
expect(screen.getByText("Blank")).toBeInTheDocument();
4141
});
4242

43-
it("switches to Publish view and shows capability rows", () => {
43+
it("switches to Publish view and shows the empty state before an app is built", () => {
4444
const { container } = render(<AppStudioApp windowId="test" />);
4545
const nav = container.querySelector("nav[aria-label='App Studio views']")!;
4646
const publishBtn = Array.from(nav.querySelectorAll("button")).find(
4747
(b) => b.getAttribute("aria-label") === "Publish"
4848
)!;
4949
fireEvent.click(publishBtn);
50-
// app identity
51-
expect(screen.getAllByText("Chore Quest").length).toBeGreaterThan(0);
52-
// capability row labels
53-
expect(screen.getByTestId("perm-row-workspace")).toBeInTheDocument();
54-
expect(screen.getByTestId("perm-row-notifications")).toBeInTheDocument();
55-
expect(screen.getByTestId("perm-row-household")).toBeInTheDocument();
56-
// publish button
57-
expect(screen.getByRole("button", { name: /publish to my store/i })).toBeInTheDocument();
50+
expect(screen.getByText(/generate an app in the build view first/i)).toBeInTheDocument();
5851
});
5952
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { useCallback, useRef } from "react";
2+
import { createPortal } from "react-dom";
3+
import { useFocusTrap } from "@/hooks/use-focus-trap";
4+
5+
interface Props {
6+
appName: string;
7+
weightsLicense: string;
8+
text: string;
9+
submitting?: boolean;
10+
error?: string | null;
11+
/** Re-POSTs the install with accepted: true. */
12+
onAccept: () => void;
13+
onDecline: () => void;
14+
}
15+
16+
/**
17+
* Single-accept license dialog shown before installing a service whose
18+
* backend pins non-commercial model weights (#169) -- e.g. musicgen's
19+
* MIT wrapper around Meta's CC-BY-NC 4.0 weights. Cloned from
20+
* PermissionConsent's modal shell (focus trap, portal, styling) but asks a
21+
* single "I agree" question about a weights license instead of a
22+
* multi-capability grant.
23+
*/
24+
export function LicenseAcceptDialog({
25+
appName,
26+
weightsLicense,
27+
text,
28+
submitting = false,
29+
error = null,
30+
onAccept,
31+
onDecline,
32+
}: Props) {
33+
const dialogRef = useRef<HTMLDivElement>(null);
34+
useFocusTrap(dialogRef, true);
35+
36+
const handleAccept = useCallback(() => {
37+
if (!submitting) onAccept();
38+
}, [submitting, onAccept]);
39+
40+
return createPortal(
41+
<div
42+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
43+
onClick={(e) => {
44+
if (submitting) return;
45+
if (e.target === e.currentTarget) onDecline();
46+
}}
47+
onKeyDown={(e) => {
48+
if (submitting) return;
49+
if (e.key === "Escape") onDecline();
50+
}}
51+
>
52+
<div
53+
ref={dialogRef}
54+
role="dialog"
55+
aria-modal="true"
56+
aria-label={`${appName} weights license`}
57+
className="bg-shell-surface border border-white/10 shadow-2xl rounded-2xl w-[26rem] max-w-[90vw] flex flex-col"
58+
>
59+
<div className="px-5 py-4 border-b border-white/5">
60+
<h2 className="text-sm font-semibold text-shell-text">
61+
{appName} uses non-commercial model weights
62+
</h2>
63+
</div>
64+
<div className="px-5 py-4 space-y-2.5">
65+
<p className="text-[13px] text-shell-text-secondary leading-relaxed">{text}</p>
66+
<p className="text-[11px] text-shell-text-tertiary">
67+
Weights license: <span className="text-shell-text font-medium">{weightsLicense || "non-commercial (label unavailable)"}</span>
68+
</p>
69+
</div>
70+
{error && (
71+
<div role="alert" className="mx-5 mb-3 text-[12px] text-red-300 bg-red-500/10 border border-red-500/20 rounded px-2 py-1">
72+
{error}
73+
</div>
74+
)}
75+
<div className="flex justify-end gap-2 px-5 py-4 border-t border-white/5">
76+
<button
77+
type="button"
78+
onClick={onDecline}
79+
disabled={submitting}
80+
className="px-4 py-1.5 rounded-full text-[13px] font-semibold text-shell-text-secondary hover:text-shell-text hover:bg-white/[0.04] transition-colors disabled:opacity-60"
81+
>
82+
Cancel
83+
</button>
84+
<button
85+
type="button"
86+
onClick={handleAccept}
87+
disabled={submitting}
88+
className="px-4 py-1.5 rounded-full text-[13px] font-bold text-white transition-colors disabled:opacity-60"
89+
style={{ background: "var(--color-accent)" }}
90+
>
91+
{submitting ? "Installing…" : "Agree & Install"}
92+
</button>
93+
</div>
94+
</div>
95+
</div>,
96+
document.body,
97+
);
98+
}
99+
100+
export default LicenseAcceptDialog;

desktop/src/apps/StoreApp/index.tsx

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { loadFilter, saveFilter } from "./storage";
1717
import { emitAppEvent, APP_INSTALLED } from "@/lib/app-event-bus";
1818
import { TaosAppsSection } from "./TaosAppsSection";
1919
import { ImportAppButton } from "./ImportAppButton";
20+
import { LicenseAcceptDialog } from "./LicenseAcceptDialog";
2021
import { useIsMobile } from "@/hooks/use-is-mobile";
2122
import { MobileStore } from "./MobileStore";
2223
import { AppIcon, StoreCover } from "./AppIcon";
@@ -419,6 +420,38 @@ function AppCard({
419420
const showVariantPicker = !app.installed && variantOptions.length > 1;
420421
const showTargetPicker = !app.installed && installTargets.length > 1;
421422

423+
interface LicenseGate { weightsLicense: string; name: string; text: string }
424+
const [licenseGate, setLicenseGate] = useState<LicenseGate | null>(null);
425+
const [licenseSubmitting, setLicenseSubmitting] = useState(false);
426+
427+
/** POST install-v2. Returns "ok" | "gated" | "error" — "gated" means the
428+
* backend returned 412 needs_license_acceptance (non-commercial weights,
429+
* #169) and licenseGate state is now set so the accept dialog renders. */
430+
const postInstall = async (accepted: boolean): Promise<"ok" | "gated" | "error"> => {
431+
const body: Record<string, unknown> = { app_id: app.id, target_remote: selectedTarget };
432+
if (selectedVariant !== "auto") body.variant_id = selectedVariant;
433+
if (accepted) body.accepted = true;
434+
const res = await fetch("/api/store/install-v2", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body) });
435+
if (res.status === 412) {
436+
try {
437+
const gate = await res.json();
438+
if (gate?.needs_license_acceptance) {
439+
setLicenseGate({ weightsLicense: String(gate.weights_license ?? ""), name: String(gate.name ?? app.name), text: String(gate.text ?? "") });
440+
return "gated";
441+
}
442+
} catch { /* fall through to generic error below */ }
443+
}
444+
if (!res.ok) { let msg = `Install failed (${res.status})`; try { const err = await res.json(); if (err?.error) msg = String(err.error); } catch { /* ignore */ } setError(msg); return "error"; }
445+
if (isFramework) {
446+
try {
447+
const data = await res.json();
448+
if (data?.prefetch === "started") { setPrefetchStatus("downloading"); setPrefetchActive(true); }
449+
} catch { /* no body / not framework prefetch */ }
450+
}
451+
onInstall(app.id);
452+
return "ok";
453+
};
454+
422455
const handleAction = async () => {
423456
setBusy(true); setError(null); setProgress(null);
424457
try {
@@ -427,23 +460,31 @@ function AppCard({
427460
if (!res.ok) { let msg = `Uninstall failed (${res.status})`; try { const err = await res.json(); if (err?.error) msg = String(err.error); } catch { /* ignore */ } setError(msg); setBusy(false); return; }
428461
onUninstall(app.id);
429462
} else {
430-
const body: Record<string, unknown> = { app_id: app.id, target_remote: selectedTarget };
431-
if (selectedVariant !== "auto") body.variant_id = selectedVariant;
432-
const res = await fetch("/api/store/install-v2", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body) });
433-
if (!res.ok) { let msg = `Install failed (${res.status})`; try { const err = await res.json(); if (err?.error) msg = String(err.error); } catch { /* ignore */ } setError(msg); setBusy(false); return; }
434-
if (isFramework) {
435-
try {
436-
const data = await res.json();
437-
if (data?.prefetch === "started") { setPrefetchStatus("downloading"); setPrefetchActive(true); }
438-
} catch { /* no body / not framework prefetch */ }
439-
}
440-
onInstall(app.id);
463+
const outcome = await postInstall(false);
464+
if (outcome === "gated") { setBusy(false); return; }
441465
}
442466
} catch (e) { setError(e instanceof Error ? e.message : "Network error"); }
443467
setBusy(false);
444468
setTimeout(() => setProgress(null), 1500);
445469
};
446470

471+
const handleLicenseAccept = async () => {
472+
setLicenseSubmitting(true);
473+
// Flip busy back on so the card's install-progress poller (keyed on busy,
474+
// and cleared by handleAction when the gate returned) runs during the real
475+
// accepted install instead of the card sitting idle behind the dialog.
476+
setBusy(true);
477+
try {
478+
const outcome = await postInstall(true);
479+
if (outcome !== "gated") setLicenseGate(null);
480+
} catch (e) { setError(e instanceof Error ? e.message : "Network error"); setLicenseGate(null); }
481+
setLicenseSubmitting(false);
482+
setBusy(false);
483+
setTimeout(() => setProgress(null), 1500);
484+
};
485+
486+
const handleLicenseDecline = () => { setLicenseGate(null); setBusy(false); };
487+
447488
const handleOpen = () => {
448489
window.dispatchEvent(new CustomEvent("taos:open-app", { detail: { app: "agents" } }));
449490
};
@@ -477,6 +518,15 @@ function AppCard({
477518
<span className="text-[11px] text-shell-text-tertiary leading-none">v{app.version}</span>
478519
</div>
479520
</div>
521+
{app.license_class === "non-commercial" && (
522+
<span
523+
className="inline-flex items-center gap-1 self-start text-[10px] font-semibold px-1.5 py-0.5 rounded-full bg-yellow-700/30 text-yellow-200"
524+
title={`Model weights licensed for non-commercial use${app.weights_license ? ` (${app.weights_license})` : ""}`}
525+
aria-label={`${app.name} uses non-commercial model weights${app.weights_license ? `: ${app.weights_license}` : ""}`}
526+
>
527+
Non-commercial weights
528+
</span>
529+
)}
480530
<p className="text-[11.5px] text-shell-text-secondary leading-relaxed flex-1">{app.description}</p>
481531
<div className="flex items-center justify-between">
482532
{app.stars ? (
@@ -568,6 +618,17 @@ function AppCard({
568618
</button>
569619
)}
570620
</div>
621+
{licenseGate && (
622+
<LicenseAcceptDialog
623+
appName={licenseGate.name}
624+
weightsLicense={licenseGate.weightsLicense}
625+
text={licenseGate.text}
626+
submitting={licenseSubmitting}
627+
error={error}
628+
onAccept={handleLicenseAccept}
629+
onDecline={handleLicenseDecline}
630+
/>
631+
)}
571632
</div>
572633
);
573634
}
@@ -967,6 +1028,9 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
9671028
tagline: a.tagline ? String(a.tagline) : undefined,
9681029
cover: a.cover ? String(a.cover) : COVER_BY_ID[String(a.id)]?.cover,
9691030
coverImage: a.coverImage ? String(a.coverImage) : COVER_BY_ID[String(a.id)]?.coverImage,
1031+
license: a.license ? String(a.license) : undefined,
1032+
weights_license: a.weights_license ? String(a.weights_license) : undefined,
1033+
license_class: a.license_class ? String(a.license_class) : undefined,
9701034
}));
9711035
// Merge homelab apps: only add those not already in the catalog
9721036
const catalogIds = new Set(normalized.map((a) => a.id));

0 commit comments

Comments
 (0)