diff --git a/CHANGELOG.md b/CHANGELOG.md index 677f33d0..714e5061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,20 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 0.1.0a11 + +- Guide AI provider connections, Lumi settings, and workflow AI profiles through + focused steps with review before creation or application. Keep unfinished + profile changes local and apply the profile and connection slot atomically. +- Let authors explicitly pass available earlier AI results as structured context + within one workflow execution, using existing compiler-checked expressions. +- Document the verified alpha10 Azure application-token, AI workflow, Lumi, and + HTTP acceptance separately from unverified interactive Entra sign-in. +- Correct browser selection tests to wait for the insertion reveal before + checking pointer reachability. +- Publish worker packages 0.1.3 pinned to core 0.1.0a11. No server behavior or + database migration changes; the schema remains `0028_files`. + ## 0.1.0a10 - Expose Lumi configuration through the paired Studio host and clarify the diff --git a/README.md b/README.md index 8a38ef35..11a60c77 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,14 @@ maps service tasks, user tasks, gateways, and timers to Weave steps. ## What you get -The **v0.1.0a10 alpha** release provides the **API, CLI, Python SDK, and Studio** +The **v0.1.0a11 alpha** release provides the **API, CLI, Python SDK, and Studio** visual workspace, with human-task inboxes, email conversations, execution management, and administration of people and access. Run Weave as a standalone service or embed it in another product. -**New in 0.1.0a9:** configure AI provider connections from Studio settings, select -Lumi's connection by name, and run Agentic workers with renewable OAuth2 credentials. +**New in 0.1.0a11:** configure AI provider connections, Lumi, and workflow AI +profiles through guided setup with review before saving. Explicitly select +earlier AI results as [shared context within one execution](docs/guides/shared-ai-context.md). See the illustrated [AI workflow guide](docs/guides/ai-workers.md) and [Lumi guide](docs/guides/lumi.md). @@ -80,7 +81,7 @@ See the illustrated [AI workflow guide](docs/guides/ai-workers.md) and Read the [capability matrix](docs/capabilities.md) for tested boundaries and live-provider checks that remain environment-specific. -Studio runs in your browser from the [installed CLI](docs/guides/studio.md#install-the-alpha10-browser-application) +Studio runs in your browser from the [installed CLI](docs/guides/studio.md#install-the-alpha11-browser-application) or as a [desktop app](docs/guides/desktop.md). The macOS desktop bundles are ad-hoc signed, not Developer ID signed or notarized, so macOS may ask you to approve them; do not use the alpha5 macOS installers, which were damaged. The @@ -118,7 +119,7 @@ integration code. Each has its own guide, so you can stop at the result you need ## Install and discover the CLI On macOS, Linux, or WSL, install **Python 3.12 or newer** with `venv` support, -then run this block in Bash or Zsh. It installs the pinned **v0.1.0a10 alpha** +then run this block in Bash or Zsh. It installs the pinned **v0.1.0a11 alpha** into your user account without `sudo`, Git, or Docker: ```sh @@ -126,12 +127,12 @@ into your user account without `sudo`, Git, or Docker: # Stop if downloading the installer fails. set -o pipefail curl --proto '=https' --tlsv1.2 -fsSL \ - https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/install.sh \ - | sh -s -- --version v0.1.0a10 + https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/install.sh \ + | sh -s -- --version v0.1.0a11 ) ``` -Expected: `Installed Firefly Weave 0.1.0a10:` followed by the command's path. Then +Expected: `Installed Firefly Weave 0.1.0a11:` followed by the command's path. Then make the default command directory available in this terminal and look around: ```sh @@ -145,7 +146,7 @@ weave help workflow weave docs platform ``` -Expected: `Firefly Weave 0.1.0a10`, the command overview, the `workflow` +Expected: `Firefly Weave 0.1.0a11`, the command overview, the `workflow` commands, and the address of the platform guide. You do not need to learn every command first: help explains each family and its next steps. The [installation guide](docs/installation.md) covers choosing Python, a permanent @@ -178,7 +179,7 @@ Compose files and setup helpers. Clone the tag that matches the CLI: ```sh # Keep the platform files at the same version as the CLI. -git clone --branch v0.1.0a10 --single-branch https://github.com/fireflyframework/firefly-weave.git +git clone --branch v0.1.0a11 --single-branch https://github.com/fireflyframework/firefly-weave.git cd firefly-weave # Check prerequisites, then prepare private settings and dependencies once. @@ -264,7 +265,7 @@ path, and the detailed diagrams. ## Current release and limits -The recommended installation is **v0.1.0a10**, an **alpha** release. Download +The recommended installation is **v0.1.0a11**, an **alpha** release. Download packages and checksums from [GitHub Releases](https://github.com/fireflyframework/firefly-weave/releases). The documentation on a branch describes the source on that branch; a release tag @@ -280,12 +281,18 @@ setup and delivery still need validation in your environment. Sign-in from the CLI, and from Studio in an opt-in browser test, has been verified end to end only against the local platform's Keycloak 26.7.4, and sign-in with a code from a locally built macOS desktop app against the same platform; browser sign-in from -the desktop app and the DMG have not been verified. Microsoft Entra ID and other -OIDC providers have configuration guidance, not verification: the built-in token -verifier accepts Entra's signing keys, which omit `alg`, but only unit tests -cover them, and no Entra token has been verified yet. Credential storage on +the desktop app and the DMG have not been verified. Credential storage on Windows and Linux is not verified. +Azure preproduction checks on alpha10 verified Microsoft Entra ID application +tokens for a host application and an independent Agentic worker. An Azure OpenAI +workflow completed through that worker with consistent replay, and Lumi returned +a response through its separate gateway. An HTTP workflow also completed after +the alpha10 deployment, and five existing run states were unchanged. These checks +do not verify Entra browser or device-code sign-in for people, other identity providers, or general +provider availability. See the [capability matrix](docs/capabilities.md) for the +verified scope. + ## Meet Lumi ![Lumi, the Firefly Weave guide, with translucent mint wings and an amber lantern](assets/lumi.png) diff --git a/desktop/RELEASE.md b/desktop/RELEASE.md index 7ed11eb2..85393bf6 100644 --- a/desktop/RELEASE.md +++ b/desktop/RELEASE.md @@ -17,16 +17,16 @@ SPDX-License-Identifier: Apache-2.0 --> # Desktop release versions -The alpha10 product version is `0.1.0-alpha.10` in npm, Cargo and Tauri. It corresponds -to Python `0.1.0a10` and the repository prerelease tag `v0.1.0a10`. +The alpha11 product version is `0.1.0-alpha.11` in npm, Cargo and Tauri. It corresponds +to Python `0.1.0a11` and the repository prerelease tag `v0.1.0a11`. -Windows MSI uses the explicit numeric version `0.1.10`. Tauri's pinned bundler rejects +Windows MSI uses the explicit numeric version `0.1.11`. Tauri's pinned bundler rejects nonnumeric prerelease identifiers when deriving MSI versions; its `windows.wix.version` override supplies the valid numeric installer version while filenames retain the product version. MSI compares only the first three fields. Future desktop alpha and stable releases must advance this numeric installer counter: the stable product `0.1.0` must not reset the MSI counter to `0.1.0`. The macOS internal bundle version also uses -numeric `0.1.10`; its displayed product version remains the alpha product version. +numeric `0.1.11`; its displayed product version remains the alpha product version. The Desktop installers workflow builds on four native runners and is dispatchable by an exact branch/tag ref. Versioned, target-specific unsigned artifacts contain only diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 6509deb5..aced224e 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firefly-weave/desktop", - "version": "0.1.0-alpha.10", + "version": "0.1.0-alpha.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firefly-weave/desktop", - "version": "0.1.0-alpha.10", + "version": "0.1.0-alpha.11", "license": "Apache-2.0", "devDependencies": { "@tauri-apps/cli": "2.12.1" diff --git a/desktop/package.json b/desktop/package.json index 19e44a5a..930f3b88 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@firefly-weave/desktop", - "version": "0.1.0-alpha.10", + "version": "0.1.0-alpha.11", "private": true, "license": "Apache-2.0", "scripts": { diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 39e8c27f..a0041c50 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -794,7 +794,7 @@ checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "firefly-weave-studio" -version = "0.1.0-alpha.10" +version = "0.1.0-alpha.11" dependencies = [ "serde", "serde_json", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 40458ac7..e417c2f6 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ [package] name = "firefly-weave-studio" -version = "0.1.0-alpha.10" +version = "0.1.0-alpha.11" description = "Firefly Weave Studio desktop" authors = ["Firefly Software Foundation"] license = "Apache-2.0" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index a1230e5f..dec374a4 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Firefly Weave Studio", - "version": "0.1.0-alpha.10", + "version": "0.1.0-alpha.11", "identifier": "io.getfirefly.weave.studio", "build": { "frontendDist": "../bootstrap" @@ -52,7 +52,7 @@ "y": 160 } }, - "bundleVersion": "0.1.10", + "bundleVersion": "0.1.11", "signingIdentity": "-", "entitlements": "entitlements.plist" }, @@ -61,7 +61,7 @@ "type": "downloadBootstrapper" }, "wix": { - "version": "0.1.10" + "version": "0.1.11" } }, "icon": [ diff --git a/docs/README.md b/docs/README.md index 805a5cbf..e287a956 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,7 +47,7 @@ gives the steps and what you finish with. | An administrator | Install the platform, configure sign-in, and give people access | [Start a local platform](guides/local-platform.md) | [Identity and secrets](operations/identity-and-secrets.md), [remote deployment](operations/remote-deployment.md), [people and access](guides/people-and-access.md), and [configuration](operations/configuration.md) | | An operator | Keep runs healthy, fix incidents, upgrade, and back up | [Connect the CLI](guides/connect-to-api.md), then [manage runs](guides/execution-management.md) | [Incidents](reference/incident-operations.md), [troubleshooting](operations/troubleshooting.md), [observability](operations/observability.md), [upgrades](operations/upgrades.md), [backup](operations/backup-restore.md), and [debug-session retention](operations/retention.md) | -Start each path by [installing the CLI](installation.md) from the v0.1.0a10 +Start each path by [installing the CLI](installation.md) from the v0.1.0a11 release. [Who does what](guides/roles-and-lifecycle.md) explains these roles with an @@ -104,7 +104,7 @@ Run one command block, check its **Expected** result, then continue. Comments inside code blocks explain why each command is there. When a terminal shows server logs, keep it open and use a second terminal for client commands. -The guides pin the **v0.1.0a10 alpha** release, a preview, which includes every +The guides pin the **v0.1.0a11 alpha** release, a preview, which includes every feature they describe, such as saved platforms, REST calls without code, and the Studio editor. When a guide shows how to work with an alpha6 or earlier client or server, it says so. diff --git a/docs/architecture.md b/docs/architecture.md index 9b584eb3..9ce88833 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -233,9 +233,10 @@ health probes, signed webhooks and provider ingress endpoints (which verify the sender by their own rules), and the interactive API documentation when it is enabled. Services enforce scoped authorization even when they are called without HTTP. The local platform uses Keycloak; a deployment can trust another OIDC -provider through `WEAVE_OIDC_PROVIDERS`. Only the local Keycloak setup has been -verified against a real provider, and the built-in verifier has a -[known limitation with Microsoft Entra ID](operations/identity-and-secrets.md#microsoft-entra-id-not-verified). +provider through `WEAVE_OIDC_PROVIDERS`. Real-provider checks cover local +Keycloak sign-in and Microsoft Entra ID application tokens in Azure +preproduction. Entra browser and device-code sign-in for people remain +[unverified](operations/identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified). **Persistence.** The [unit of work](../src/firefly_weave/persistence/uow.py) binds the tenant context inside a PostgreSQL transaction. Repositories use that diff --git a/docs/capabilities.md b/docs/capabilities.md index 823ea512..34b82d2c 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -51,7 +51,7 @@ permissions, and real delivery still need checking in your deployment. ## Which release this page describes Weave is an alpha workflow and integration platform. This branch prepares release -**`0.1.0a10`**, with PyFly 26.9.15 and database schema revision +**`0.1.0a11`**, with PyFly 26.9.15 and database schema revision `0028_files`; published packages are on [GitHub Releases](https://github.com/fireflyframework/firefly-weave/releases). It includes [Studio](guides/studio.md), [human tasks and operator @@ -70,8 +70,8 @@ releases do not have. Alpha8 adds three migrations; see the | [Files](guides/files.md) | New in alpha8: scoped, resumable transfer API, SDK/CLI helpers, Studio fields and human-task attachments | PostgreSQL lease/claim/retention checks; checksum and chunk tests; Studio browser tests | Not applicable to platform storage | 25 MiB per file; content stored in PostgreSQL; no automatic abandoned-upload cleanup | | [File integrations](guides/file-connectors.md) | FTP, FTPS, SFTP, SharePoint/OneDrive and Google Drive workers | Real local FTP/FTPS/SFTP servers; simulated Graph and Drive responses | Microsoft and Google accounts not exercised | Six explicit operations; no standalone change-feed trigger yet | | [Decision tables](reference/decision-tables.md) | Versioned decision definitions and a dedicated workflow step | Compiler, API and PostgreSQL execution tests | Not applicable | Deterministic rule evaluation; no arbitrary expression execution | -| [AI tasks](guides/ai-workers.md) | Per-workflow model profiles executed by an independent Agentic worker | Real Agentic library with controlled model transport; PostgreSQL completion/replay and secret classification | No live model provider claimed | Exact operator model/endpoint policy; external requests can be ambiguous | -| [Lumi assistant](guides/lumi.md) | Separate environment profile and private gateway; opt-in context and reviewed Studio draft proposals | API authorization tests; 7-size Studio browser checks | No live model provider claimed | No autonomous publish, activation, or execution; conversation stays in memory | +| [AI tasks](guides/ai-workers.md) | Per-workflow model profiles executed by an independent Agentic worker | Real Agentic library with controlled model transport; PostgreSQL completion/replay and secret classification | Azure OpenAI workflow completed on alpha10 through an independent Agentic worker; accepted result and consistent replay checked | Exact operator model/endpoint policy; external requests can be ambiguous | +| [Lumi assistant](guides/lumi.md) | Separate environment profile and private gateway; opt-in context and reviewed Studio draft proposals | API authorization tests; 7-size Studio browser checks | Azure OpenAI response through the separate Lumi gateway on alpha10; installed alpha10 Studio configuration readback against Azure preproduction | No autonomous publish, activation, or execution; conversation stays in memory | | [Compiler and schemas](reference/compiler.md) | Implemented | Compiler, schema, expression, and canonical-artifact suites | Not applicable | Validation without a catalog is partial and produces no artifact | | [Definition lifecycle](reference/api.md) | Implemented | Publication, activation, revision, and scoped API suites | Not applicable | Compiling is separate from authorization and admission | | [Durable runtime](reference/schedules-and-timers.md) | Implemented | PostgreSQL, lease, wait, signal, parallel, schedule, and recovery scenarios | Not applicable | External effects can repeat after a crash | @@ -79,11 +79,11 @@ releases do not have. Alpha8 adds three migrations; see the | [Execution management](guides/execution-management.md) | Implemented | Run lifecycle scenarios: search by business key, archive, restore, and purge | Not applicable | Purge needs an archived, finished run and a separate permission | | [Workers](reference/worker-protocol.md) | Implemented | Native and remote worker protocol scenarios | Not applicable | Release identity and scoped grants must be provisioned | | [Simulation](reference/simulation.md), [history and replay](reference/history-and-replay.md), [incidents](reference/incident-operations.md) | Implemented | Mock simulation and durable evidence and control scenarios | Not applicable | Replay can report incomplete evidence; simulation never calls providers | -| [Studio](guides/studio.md) in the browser | Implemented; the connection steps, saved platforms, and the redesigned editor are **new in 0.1.0a7** | Unit tests; browser tests against a mocked platform API and a real local host; an opt-in browser test against a local platform and its Keycloak on macOS (connect, sign in, renew, switch account, sign out, and a REST call built in Studio) | Other identity providers, and platforms other than the local one | Studio is a client: it grants nothing, and pairing is not a sign-in | +| [Studio](guides/studio.md) in the browser | Implemented; the connection steps, saved platforms, and the redesigned editor are **new in 0.1.0a7** | Unit tests; browser tests against a mocked platform API and a real local host; an opt-in browser test against a local platform and its Keycloak on macOS (connect, sign in, renew, switch account, sign out, and a REST call built in Studio) | Installed alpha10 AI setup against Azure preproduction using an application identity; interactive Entra human sign-in not exercised | Studio is a client: it grants nothing, and pairing is not a sign-in | | [Studio desktop](guides/desktop.md) | Implemented for macOS, Windows, and Linux targets | Native shell and host unit tests; exports checked in a macOS WebKit test harness; a locally built macOS app against the local Keycloak 26.7.4: sign-in with a code, reconnection after a restart, and **Save to file** | Browser sign-in from the app and the DMG not verified; not run on Windows or Linux | macOS bundles are ad-hoc signed, not notarized; Windows installers are unsigned | | [Saved platforms and CLI sign-in](guides/connect-to-api.md) | **New in 0.1.0a7**: `weave auth setup`, saved platforms shared by the CLI, Studio, and desktop app | A real run against the local Keycloak, keeping tokens in a private credential file; Studio's opt-in browser test and the locally built macOS app used the macOS Keychain | Windows Credential Locker, Secret Service, and KWallet were not exercised end to end; they are covered only by unit tests with simulated backends | Saved platforms hold no secrets; a sign-in alone grants no access | | [Keycloak identity](operations/identity-and-secrets.md#local-keycloak-development) | Implemented, including published sign-in settings for the CLI and Studio | Owned Keycloak 26.7.4: CLI browser sign-in with PKCE and device sign-in, renewal with refresh-token rotation, revocation, unlinked and no-access accounts, local authorization scenarios | No customer realm certification; in the desktop app, only sign-in with a code was verified | Explicit identity links and local grants remain required | -| [Generic OIDC / Entra ID](operations/identity-and-secrets.md#provider-notes) | Configurable token verification and published sign-in settings; provider-neutral ports and claim mapping present | Local contract coverage with simulated providers | Entra ID and other CIAM not verified; signing keys without `alg`, as Entra publishes them, are accepted by key type, which only unit tests cover | No automatic provisioning, group-overage expansion, or provider portability guarantee | +| [Generic OIDC / Entra ID](operations/identity-and-secrets.md#provider-notes) | Configurable token verification and published sign-in settings; provider-neutral ports and claim mapping present | Local contract coverage with simulated providers | Real Entra application tokens verified for a host application and independent Agentic worker in Azure preproduction; Entra human browser/device sign-in and other CIAM remain unverified | No automatic provisioning, group-overage expansion, or provider portability guarantee | | [REST calls without code](connectors/http-without-code.md) | **New in 0.1.0a7**: `weave connector http-action`, `import-openapi --target builtin`, guided `weave connections create`, and Studio's API action builder, on the built-in `weave-http@2.0.0` | Offline Action and import checks, compile-time profile checks, and the guide's local platform example; the guide states what ran end to end | No commercial API account contacted | One JSON operation over HTTPS per Action; only the connection's listed origins; private networks refused unless an operator allows them | | [Local platform](guides/local-platform.md) (`weave platform`) | Implemented; `user`, `integrations`, and `secret` are **new in 0.1.0a7** | Runs on macOS with a local Docker engine and Keycloak 26.7.4 | Not applicable | Development only, on macOS or Linux with a Unix-socket Docker context | | [HTTP and webhooks](reference/http-and-webhooks.md) | Implemented | Bounded transport, signed ingress, and owned endpoint scenarios | Not applicable to arbitrary destinations | Egress and scoped credential policy apply | @@ -98,7 +98,7 @@ releases do not have. Alpha8 adds three migrations; see the | [Telegram webhook text](connectors/telegram.md) | Implemented | Local TLS fixtures and owned PostgreSQL; composed provider gate passed | Not run | Source-local deduplication; bot and group permissions are provider prerequisites | | [Operational limits](operations/configuration.md), [telemetry](operations/observability.md), [retention](operations/retention.md), and [compatibility](operations/upgrades.md) | Implemented | Policy, accounting, contention, telemetry, retention, and upgrade suites | Not applicable | Logical accounting is separate from physical storage; compatibility checks gate readiness | | [Deployment](operations/deployment.md), [backup and restore](operations/backup-restore.md), and distribution | Implemented | Installed-package, container, process recovery, restore, and queue suites | Not applicable | Restore ownership, grants, schema compatibility, and external systems must be checked in each environment | -| [AWS, Azure, and GCP deployment recipes](operations/cloud-deployment.md) | Reference guides and Kubernetes manifests | Source review and offline Kubernetes object-shape validation | Not run | Operator-provisioned infrastructure; database migrations, identity, networking, and recovery need acceptance in the target | +| [AWS, Azure, and GCP deployment recipes](operations/cloud-deployment.md) | Reference guides and Kubernetes manifests | Source review and offline Kubernetes object-shape validation | A separate Azure Container Apps deployment has been exercised; these Kubernetes recipes and AWS/GCP remain unverified | Operator-provisioned infrastructure; database migrations, identity, networking, and recovery need acceptance in the target | | Slack and Salesforce | Deferred | Not run | Not run | Not part of this delivery | ## What is not included diff --git a/docs/diagrams/shared-ai-context.svg b/docs/diagrams/shared-ai-context.svg new file mode 100644 index 00000000..43eb6e25 --- /dev/null +++ b/docs/diagrams/shared-ai-context.svg @@ -0,0 +1,53 @@ + + + +AI steps share selected results within one execution +A summarizer saves a validated result. The workflow can pause or recover. A later AI task reads the selected saved result together with its own context. Other executions and Lumi conversations remain separate. + + + +Firefly Weave · Shared AI context +One execution. Selected, saved context. +You choose what the next model receives. + +Inside this workflow run + + +1. Summarize the request +The AI task returns a result that passes its schema. +Weave accepts and saves that result with the run. + + + +2. Keep the result through a pause +A timer or human task can pause the workflow. +The saved summary does not live in worker memory. + + + +3. Write a reply with selected context +The next task reads the accepted summary. + +data: the reply instructions you supplied +sharedAiResults.summarize: the saved summary +Ordinary expressions, visible in YAML and Python. + +Separate from other runs and Lumi conversations +No implicit transcript or shared provider session is added. + diff --git a/docs/guides/ai-workers.md b/docs/guides/ai-workers.md index 84001bd1..b29d1a57 100644 --- a/docs/guides/ai-workers.md +++ b/docs/guides/ai-workers.md @@ -27,7 +27,7 @@ Use [Lumi](lumi.md) when *you*, the person using Studio, want an explanation or a proposed definition change. Lumi has separate configuration and does not execute workflow AI tasks. -The examples here use Weave **0.1.0a10** and the **0.1.2 Agentic worker package**, +The examples here use Weave **0.1.0a11** and the **0.1.3 Agentic worker package**, whose core dependency is pinned to that Weave version. The catalog references below retain their own `1.0.0` definition and task versions. @@ -213,18 +213,26 @@ available from **Connections** and the AI task inspector. You need `connection.manage` in the environment. If the provider Connector is not yet published, the form explains the prerequisite; an author cannot bypass it. -1. Enter a **Connection name** that identifies the intended account or purpose. -2. Choose **Provider** and enter its approved **Provider endpoint**. For an Azure - provider, also enter the explicit **Azure API version**. The model or Azure - deployment name belongs in the workflow profile, not this connection form. -3. Enter the **API key secret handle** supplied by the operator. This field takes - a handle, never the raw API key. -4. Select **Create AI connection**. Studio restricts allowed destinations to the - endpoint's exact HTTPS origin. The operator's worker policy must also permit - that endpoint. -5. Record the created connection and revision. The platform checked its - configuration; no model request was sent. Complete the worker release and - credential grants before binding it to an executable workflow. +Studio alpha11 guides this setup through three steps. Alpha10 presents the +same fields in a single form. + +1. **Provider:** enter a **Connection name** that identifies the intended account + or purpose, choose **Provider**, and select **Continue**. The model or Azure + deployment name belongs in the workflow profile. +2. **Access:** copy the exact operator-approved **Provider endpoint**. For Azure, + also enter the explicit **Azure API version**. Enter the **API key secret + handle** supplied by the operator, never the raw API key, then select + **Review connection**. The Azure endpoint hint has no trailing slash; copy + the approved endpoint exactly rather than inventing a different spelling. +3. **Review:** check the name, provider, endpoint, API version when applicable, + and secret handle. **Back** preserves these details so you can correct them. + Select **Create AI connection** only when they are ready. Earlier steps do + not create a connection or send a model request. +4. Record the created connection and revision. Studio restricts allowed + destinations to the endpoint's HTTPS origin, and the operator's worker policy + must also permit the endpoint. The platform checked configuration, not live + provider connectivity. Complete worker release and credential grants before + binding it to an executable workflow. For API clients, create a connection for the published provider Connector with this configuration: @@ -405,6 +413,12 @@ The platform integration tests additionally activate the canonical catalog, claim a real database-backed task, enforce the pinned credential grant, and verify completion, output guards, replay, and classified-result rejection. +A separate Azure preproduction check on alpha10 used real Microsoft Entra ID +application tokens and an independently deployed Agentic worker to complete an +Azure OpenAI workflow. Its accepted result replayed consistently. This verifies +that configured deployment and model path; it does not certify other providers, +models, environments, or interactive Entra sign-in for people. + ## Check readiness one layer at a time | What you see | What to check next | @@ -416,5 +430,9 @@ verify completion, output guards, replay, and classified-result rejection. | The task waits without being claimed | Check that the independently deployed worker is running, authenticated, and has capacity for its admitted release. | | The call fails with a safe LLM error | Check the allowed model/endpoint, provider access, output contract, and configured budgets. A generic connection test does not validate remote provider credentials. | +To pass results between AI steps in one workflow execution, follow +[Share context between AI steps](shared-ai-context.md). Context is explicit +workflow data; it is not a persistent conversation shared by unrelated runs. + For assistance while editing, continue with [Lumi](lumi.md). Enabling Lumi is a separate setup; it neither starts this worker nor changes any workflow profile. diff --git a/docs/guides/connect-to-api.md b/docs/guides/connect-to-api.md index 3959bbd8..f37878c6 100644 --- a/docs/guides/connect-to-api.md +++ b/docs/guides/connect-to-api.md @@ -71,8 +71,9 @@ to the CLI that started it), sign-in with a code (device flow), silent renewal, switching account, an account the platform does not know, an account without a workspace, several saved platforms, and sign-out with revocation. That run kept its tokens in a [private credential file](#what-is-saved-and-where), not in a -system credential store. Microsoft Entra ID and other identity providers have -not been verified; see +system credential store. Entra application tokens have separately been verified +in Azure preproduction; Entra human sign-in and other identity providers remain +unverified. See [Use Microsoft Entra ID or another identity provider](#use-microsoft-entra-id-or-another-identity-provider). ## Choose how you connect @@ -842,15 +843,18 @@ release. ## Use Microsoft Entra ID or another identity provider -**Not verified.** Only the local platform's Keycloak 26.7.4 has been tested end -to end. Treat everything else as configuration guidance, and test with one -account before you roll out. Your administrator configures the server side by +**Human sign-in remains unverified for Entra ID.** Browser and device-code +sign-in have been tested end to end against the local platform's Keycloak +26.7.4. Separate Azure preproduction checks verified Entra application tokens +for a host application and an independent worker; these do not verify a +person's interactive sign-in. Test with one account before you roll out. Your administrator configures the server side by following [Use your own identity provider](../operations/identity-and-secrets.md#use-your-own-identity-provider). For **Microsoft Entra ID**, read -[Microsoft Entra ID (not verified)](../operations/identity-and-secrets.md#microsoft-entra-id-not-verified) -first. The built-in token verifier accepts Entra's signing keys, but no Entra -token has been verified yet, so try it with one account first. +[Microsoft Entra ID (human sign-in not verified)](../operations/identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) +first. The built-in token verifier has accepted real Entra application tokens; +the human account, consent, browser/device flow, refresh, and sign-out paths +still need their own acceptance checks. Whatever the provider, the CLI checks these points while it connects and signs in. Each failure has its own support code: diff --git a/docs/guides/desktop.md b/docs/guides/desktop.md index 75237a0e..28951607 100644 --- a/docs/guides/desktop.md +++ b/docs/guides/desktop.md @@ -44,31 +44,31 @@ start them: ## Choose an installer Choose a matching installer **actually attached** to the -[v0.1.0a10 release](https://github.com/fireflyframework/firefly-weave/releases/tag/v0.1.0a10). -The desktop product version is `0.1.0-alpha.10`; its bundled Python host is -`0.1.0a10`. macOS bundles use **ad-hoc signing** for resource integrity; they are +[v0.1.0a11 release](https://github.com/fireflyframework/firefly-weave/releases/tag/v0.1.0a11). +The desktop product version is `0.1.0-alpha.11`; its bundled Python host is +`0.1.0a11`. macOS bundles use **ad-hoc signing** for resource integrity; they are **not Developer ID signed and not notarized**. Windows installers remain unsigned. Build and frozen-host smoke checks do not establish interactive GUI testing on every operating system. Your organization's installation policy still applies. | Computer | Target | Installer filename | | --- | --- | --- | -| macOS Apple silicon | `aarch64-apple-darwin` | `firefly-weave-studio-0.1.0-alpha.10-aarch64-apple-darwin.dmg` | -| macOS Intel | `x86_64-apple-darwin` | `firefly-weave-studio-0.1.0-alpha.10-x86_64-apple-darwin.dmg` | -| Windows x64 | `x86_64-pc-windows-msvc` | `firefly-weave-studio-0.1.0-alpha.10-x86_64-pc-windows-msvc.exe` or `.msi` | -| Linux x64 | `x86_64-unknown-linux-gnu` | `firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.deb` or `.AppImage` | +| macOS Apple silicon | `aarch64-apple-darwin` | `firefly-weave-studio-0.1.0-alpha.11-aarch64-apple-darwin.dmg` | +| macOS Intel | `x86_64-apple-darwin` | `firefly-weave-studio-0.1.0-alpha.11-x86_64-apple-darwin.dmg` | +| Windows x64 | `x86_64-pc-windows-msvc` | `firefly-weave-studio-0.1.0-alpha.11-x86_64-pc-windows-msvc.exe` or `.msi` | +| Linux x64 | `x86_64-unknown-linux-gnu` | `firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.deb` or `.AppImage` | Download the chosen installer and its target's checksum inventory, `weave-studio-TARGET-SHA256SUMS`, from that same release. The matrix describes filenames, not a promise that every asset is already available. If your matching -asset is absent, use the [browser installation](studio.md#install-the-alpha10-browser-application) +asset is absent, use the [browser installation](studio.md#install-the-alpha11-browser-application) instead. Alpha4 Python releases do not contain Studio or desktop installers. **Do not use the alpha5 macOS installers.** Their outer application bundle lacked its resource seal, causing macOS to report that the application was damaged. Alpha6 added explicit ad-hoc bundle signing and a strict verification gate for both -the built application and the application enclosed in its DMG, and alpha10 keeps -them. Download the matching alpha10 asset rather than attempting to bypass the +the built application and the application enclosed in its DMG, and alpha11 keeps +them. Download the matching alpha11 asset rather than attempting to bypass the alpha5 failure. Ad-hoc integrity does not establish publisher trust or Gatekeeper acceptance. @@ -110,9 +110,9 @@ This follows Tauri's [sidecar model](https://v2.tauri.app/develop/sidecar/) and ```sh # Inspect the downloaded installer without opening it. cd ~/Downloads - shasum -a 256 firefly-weave-studio-0.1.0-alpha.10-aarch64-apple-darwin.dmg + shasum -a 256 firefly-weave-studio-0.1.0-alpha.11-aarch64-apple-darwin.dmg # Show the expected digest for this exact filename. - grep 'firefly-weave-studio-0.1.0-alpha.10-aarch64-apple-darwin.dmg$' weave-studio-aarch64-apple-darwin-SHA256SUMS + grep 'firefly-weave-studio-0.1.0-alpha.11-aarch64-apple-darwin.dmg$' weave-studio-aarch64-apple-darwin-SHA256SUMS ``` Both displayed digests must match. For Intel, replace `aarch64-apple-darwin` @@ -149,9 +149,9 @@ matching `weave-studio-x86_64-pc-windows-msvc-SHA256SUMS` inventory first. ```powershell # Inspect the downloaded EXE before running it. Set-Location "$HOME\Downloads" -Get-FileHash .\firefly-weave-studio-0.1.0-alpha.10-x86_64-pc-windows-msvc.exe -Algorithm SHA256 +Get-FileHash .\firefly-weave-studio-0.1.0-alpha.11-x86_64-pc-windows-msvc.exe -Algorithm SHA256 # Compare the digest with the line for this exact filename. -Select-String -Path .\weave-studio-x86_64-pc-windows-msvc-SHA256SUMS -Pattern 'firefly-weave-studio-0.1.0-alpha.10-x86_64-pc-windows-msvc.exe$' +Select-String -Path .\weave-studio-x86_64-pc-windows-msvc-SHA256SUMS -Pattern 'firefly-weave-studio-0.1.0-alpha.11-x86_64-pc-windows-msvc.exe$' ``` Expected: `Get-FileHash` prints a `Hash` value, and `Select-String` prints the @@ -159,8 +159,8 @@ inventory line for the same file. For MSI, substitute `.msi` in both commands. Compare the two digests without regard to letter case; stop on a mismatch. Double-click the verified installer, complete its setup, then open **Firefly Weave Studio** from the Start menu. The installer may need network access to provision WebView2. Unsigned-installation warnings remain -subject to your organization's policy. The MSI's internal version is `0.1.10`, a -monotonic Windows Installer counter; the displayed product remains alpha10. +subject to your organization's policy. The MSI's internal version is `0.1.11`, a +monotonic Windows Installer counter; the displayed product remains alpha11. ## Install on Linux @@ -171,8 +171,8 @@ application where your distribution supports its runtime. Download the matching ```sh # Enter the download directory, verify this exact file, then install only on success. cd ~/Downloads && - grep 'firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.deb$' weave-studio-x86_64-unknown-linux-gnu-SHA256SUMS | sha256sum --check && - sudo apt install ./firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.deb + grep 'firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.deb$' weave-studio-x86_64-unknown-linux-gnu-SHA256SUMS | sha256sum --check && + sudo apt install ./firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.deb ``` Expected: `sha256sum` prints the file name followed by `OK`, and only then does @@ -182,9 +182,9 @@ application launcher. For AppImage: ```sh # Verify this exact portable file; a failed or missing checksum prevents launch. cd ~/Downloads && - grep 'firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.AppImage$' weave-studio-x86_64-unknown-linux-gnu-SHA256SUMS | sha256sum --check && - chmod +x firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.AppImage && - ./firefly-weave-studio-0.1.0-alpha.10-x86_64-unknown-linux-gnu.AppImage + grep 'firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.AppImage$' weave-studio-x86_64-unknown-linux-gnu-SHA256SUMS | sha256sum --check && + chmod +x firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.AppImage && + ./firefly-weave-studio-0.1.0-alpha.11-x86_64-unknown-linux-gnu.AppImage ``` Linux requires the distribution's WebKitGTK runtime, and AppImage execution may @@ -479,7 +479,7 @@ with a text editor. **Untested platform cases.** On macOS 11.0 to 11.2 the webview lacks the download support the app relies on, so an export may save nothing even though the confirmation appears; use macOS 11.3 or later, or the -[browser installation](studio.md#install-the-alpha10-browser-application). macOS +[browser installation](studio.md#install-the-alpha11-browser-application). macOS may ask whether Firefly Weave Studio can use your Downloads folder the first time you export. On Windows, WebView2 may ask whether to allow the second of the two files. @@ -633,8 +633,8 @@ source checkout, you can run that same gate on an explicitly downloaded DMG: ```sh # Verify resource integrity without launching the application. -# Replace the path with the actual alpha10 installer you downloaded. -.venv/bin/python desktop/scripts/verify_macos_bundle.py /path/to/firefly-weave-studio-0.1.0-alpha.10-aarch64-apple-darwin.dmg +# Replace the path with the actual alpha11 installer you downloaded. +.venv/bin/python desktop/scripts/verify_macos_bundle.py /path/to/firefly-weave-studio-0.1.0-alpha.11-aarch64-apple-darwin.dmg ``` The build also smoke-tests the frozen host after Tauri signs it. Hardened runtime diff --git a/docs/guides/learning-path.md b/docs/guides/learning-path.md index 010a2dff..84ff9357 100644 --- a/docs/guides/learning-path.md +++ b/docs/guides/learning-path.md @@ -65,7 +65,7 @@ have what you need. One person often has several roles, especially on a laptop. [Who does what](roles-and-lifecycle.md) explains the roles and the permissions each one needs. -**Every path starts with the v0.1.0a10 release.** [Install the CLI](../installation.md) +**Every path starts with the v0.1.0a11 release.** [Install the CLI](../installation.md) first. The release includes everything these paths use: the Studio editor and its API action builder, saved platforms (`weave auth setup`), REST calls without code, and `weave platform user`. @@ -75,7 +75,7 @@ code, and `weave platform user`. You draw processes, add approvals and API calls, and run them. **What you need:** the CLI and -[Studio's browser application](studio.md#install-the-alpha10-browser-application), +[Studio's browser application](studio.md#install-the-alpha11-browser-application), or the [desktop app](desktop.md), and, from step 4 on, a platform: your team's server address and an account, or a [local platform](local-platform.md), which needs Docker. @@ -84,7 +84,7 @@ needs Docker. | --- | --- | --- | | 1 | [Concepts](../concepts.md), including [Coming from BPM/BPMN](../concepts.md#coming-from-bpmbpmn) | The vocabulary: workflow, step, activation, run, and how BPMN ideas map to them | | 2 | [Your first workflow](../quickstart.md) | A workflow validated and simulated on your computer, with no platform | -| 3 | [Install Studio](studio.md#install-the-alpha10-browser-application), then [draw your first workflow](studio.md#try-it-draw-your-first-workflow) | A process drawn in the visual editor and checked as you edit | +| 3 | [Install Studio](studio.md#install-the-alpha11-browser-application), then [draw your first workflow](studio.md#try-it-draw-your-first-workflow) | A process drawn in the visual editor and checked as you edit | | 4 | [Start a local platform](local-platform.md), its steps 1 to 8, or [connect Studio](studio.md#connect-to-a-platform) to your team's platform | A platform where you can publish, run, and approve; on the local platform, create your person with every role, as its step 5 explains | | 5 | [Step reference](studio-step-reference.md) | The right properties for each kind of step | | 6 | [Human tasks](human-tasks.md) | An approval assigned to real reviewers; a task manager creates the reviewer binding with the CLI | @@ -116,8 +116,9 @@ You install the platform, configure sign-in, and give people access. **What you need:** the CLI and Docker to practise locally; later, a cloud account or a Kubernetes cluster, your identity provider's administration, and a database administrator. **Planning Microsoft Entra ID?** It has -configuration guidance but has not been verified yet; -[read the requirements](../operations/identity-and-secrets.md#microsoft-entra-id-not-verified) +verified application-token checks in Azure preproduction, but browser and +device-code sign-in for people remain unverified; +[read the requirements](../operations/identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) before you deploy. | Step | Read | You finish with | diff --git a/docs/guides/local-platform.md b/docs/guides/local-platform.md index 310c1682..268eaf06 100644 --- a/docs/guides/local-platform.md +++ b/docs/guides/local-platform.md @@ -79,8 +79,8 @@ decides which features the platform has. With the alpha7 CLI from directory, and keep any checkout you already use for development untouched: ```sh -# Download the alpha10 release's operator files into a new directory. -git clone --branch v0.1.0a10 --single-branch https://github.com/fireflyframework/firefly-weave.git firefly-weave-local +# Download the alpha11 release's operator files into a new directory. +git clone --branch v0.1.0a11 --single-branch https://github.com/fireflyframework/firefly-weave.git firefly-weave-local # Run the following steps from that matching checkout. cd firefly-weave-local ``` @@ -345,7 +345,7 @@ Studio opens already connected to the platform you saved in step 6. **First install Studio's browser application** if you have not yet: download the matching alpha7 bundle and install it as in -[Install the alpha10 browser application](studio.md#install-the-alpha10-browser-application). +[Install the alpha11 browser application](studio.md#install-the-alpha11-browser-application). Then start Studio: ```sh diff --git a/docs/guides/lumi.md b/docs/guides/lumi.md index 07ea3226..c9e417e3 100644 --- a/docs/guides/lumi.md +++ b/docs/guides/lumi.md @@ -29,7 +29,7 @@ Lumi has its own configuration per environment. It never reads a workflow's `llmProfiles`, and changing an LLM step does not change the assistant. The Studio **AI setup** examples and named provider-connection selection -shown here use Weave **0.1.0a10**. Use the matching **0.1.2 Agentic worker +shown here use Weave **0.1.0a11**. Use the matching **0.1.3 Agentic worker package** for the independently deployed Lumi gateway. ## Follow a question through review @@ -178,16 +178,19 @@ separate from the administrator's editable environment Lumi configuration. 1. Open **Settings → AI setup → Configure Lumi**, or open **Lumi settings** from the assistant. These settings belong to the current environment. -2. Choose the provider and an explicit model in the Lumi profile. For Azure, +2. In Studio alpha11, the **Model → Connection → Review** wizard + keeps changes local until **Save Lumi settings**. Alpha10 presents + these fields in a single form. Choose the provider and an explicit model. For Azure, supply the Azure deployment name. Set the model's token, call, and time limits; - Studio supplies the fixed reply schema. + Studio supplies the fixed reply schema. Select **Continue to connection**. 3. Choose **Provider connection**. The list displays the connection name and revision and filters for the selected provider. Lumi pins the exact revision, so a later connection revision does not silently change the assistant. 4. If needed, select **New AI connection** and follow the [provider connection steps](ai-workers.md#configure-the-provider-connection). Use **Refresh connections** to reload available choices. -5. Enable the assistant and select **Save Lumi settings**. Use a simple request +5. Select **Review settings**, check the model and pinned connection, then select + **Save Lumi settings**. **Back** preserves your draft. Use a simple request without attachments to confirm the complete path, as described [below](#confirm-setup-without-sharing-sensitive-source). @@ -275,6 +278,13 @@ prove future model availability or the correctness of every proposed definition. Then try an explicitly selected, nonsensitive draft and practice reviewing, validating, applying, and undoing one proposed workflow change. +A separate Azure preproduction check on alpha10 completed a real Azure OpenAI +request through the Lumi gateway. Installed alpha10 Studio was also used at +desktop and narrow widths to read back Lumi configuration using an application +identity. +These checks do not establish interactive Entra sign-in for people or prove +that every generated proposal is correct. + | Problem | Responsible next step | | --- | --- | | Lumi is unavailable | The administrator checks the environment configuration and `lumi.use` grant; the operator checks that the API has a configured gateway. | diff --git a/docs/guides/shared-ai-context.md b/docs/guides/shared-ai-context.md new file mode 100644 index 00000000..27a68b7f --- /dev/null +++ b/docs/guides/shared-ai-context.md @@ -0,0 +1,203 @@ + + +# Share context between AI steps + +Use shared AI context when a later model task needs an earlier model's accepted +answer. For example, one task summarizes a customer request and a later task +writes a reply using that summary. Both tasks belong to the **same workflow run**. + +![An AI task saves a validated summary, the workflow can pause, and a later AI task reads the saved summary within the same execution](../diagrams/shared-ai-context.svg) + +[Open the diagram at full size](../diagrams/shared-ai-context.svg). + +## What “memory” means here + +Weave saves each successful step's validated output with its execution. A later +step reads that output through a reference such as +`/steps/summarize/output/result`. The reference resolves inside the current run; +it cannot address another run. This is explicit, structured context sharing. + +There is no implicit conversation transcript, background memory service, or +provider session shared between tasks. Each task starts a fresh bounded Agentic +invocation. The worker receives the prompt, the chosen context, and its pinned +model profile. Lumi conversations remain separate. + +| Setting | What it shares | +| --- | --- | +| The same workflow AI profile | Provider, model, result schema, and generation limits. It does not share answers. | +| A result selected in **Shared AI context** | An earlier AI step's accepted result, in this execution only. | +| A context expression | Precisely the workflow input or earlier output fields you select. | +| Lumi configuration | The Studio assistant's own provider and model. It does not add workflow memory. | + +## Configure it in Studio + +Start with a workflow containing two **AI task** steps in sequence, named +`summarize` and `write-reply`. Complete the [AI worker setup](ai-workers.md) +before expecting live model calls. + +1. Select `summarize`. Configure its workflow AI profile with the model, limits, + and expected result fields. Choose the AI connection slot and review the + configuration before applying it. +2. Give it a prompt such as “Summarize the request in one sentence.” Set its + context to the relevant workflow input. +3. Select `write-reply`. In **Shared AI context**, check **Use result from + summarize**. Only earlier AI results available on this execution path appear. +4. Write a prompt that names the context you are providing: “Draft a reply using + `sharedAiResults.summarize` and the instructions in `data`.” +5. Keep any additional input in the ordinary **Context** editor. When you select + a shared result, Studio preserves the existing expression under `data` and + places the selected results under `sharedAiResults`. +6. Validate the workflow. Publish, bind the connection slot during activation, + and start a run using the normal [CLI or Studio workflow](cli-tutorial.md). +7. Inspect the first task's accepted output and the second task's input in the + run details. Verify that only the intended context was sent. + +You can select up to 16 earlier AI results in the convenience picker. Unchecking +the last selection restores the original context expression. If a referenced +step is deleted or moved out of scope, Studio flags that unavailable result; +remove it or repair the workflow before running. The compiler remains the final +authority for scope, schema, classification, and payload limits. + +## Write the same workflow in YAML + +Save the following as `shared-ai-context.yaml`. Replace `approved-model` with a +model or deployment that your operator permits. This example needs the published +Agentic Action and Connector described in [AI workers](ai-workers.md). + +```yaml +apiVersion: weave/v1alpha1 +kind: Workflow +metadata: + name: shared-ai-context + version: 1.0.0 +spec: + inputSchema: + type: object + properties: + request: {type: string} + required: [request] + outputSchema: {type: string} + connections: + # Activation chooses an authorized connection revision for this slot. + ai: {connector: weave-agentic-provider@1.0.0} + llmProfiles: + writer: + provider: openai-chat + model: approved-model + options: {max_tokens: 256} + reasoning: {pattern: none} + maxCalls: 1 + timeoutSeconds: 60 + outputSchema: {type: string} + steps: + - id: summarize + kind: llm + uses: weave-agentic-generate@1.0.0 + profile: writer + connection: ai + prompt: {literal: "Summarize the supplied request in one sentence."} + context: {ref: /input/request} + - id: pause + kind: wait + # The summary stays in the run while the process waits. + durationSeconds: 10 + - id: write-reply + kind: llm + uses: weave-agentic-generate@1.0.0 + profile: writer + connection: ai + prompt: + literal: "Use sharedAiResults.summarize to draft a reply. Follow data." + context: + object: + # Keep ordinary task context alongside explicitly selected results. + data: {literal: "Use a concise, professional tone."} + sharedAiResults: + object: + summarize: {ref: /steps/summarize/output/result} + output: {ref: /steps/write-reply/output/result} +``` + +`data` and `sharedAiResults` are ordinary object keys, not new runtime APIs. The +Studio picker produces this expression shape so you can inspect and edit the +same definition in YAML, JSON, or Python. You can use a direct `ref` instead +when the task needs only one earlier result. + +## Use the Python SDK + +The SDK uses the same contracts. If `builder` is your existing +`WorkflowBuilder`, with a `writer` profile, an `ai` connection slot, and an +earlier `summarize` step, append the consumer this way: + +```python +from firefly_weave.contracts.definitions import ( + LLMStep, + LiteralExpression, + ObjectExpression, + RefExpression, +) + +# The builder is immutable: keep the returned updated workflow. +builder = builder.add_step( + LLMStep( + id="write-reply", + kind="llm", + uses="weave-agentic-generate@1.0.0", + profile="writer", + connection="ai", + prompt=LiteralExpression( + literal="Draft a reply using sharedAiResults.summarize and data." + ), + context=ObjectExpression( + object={ + "data": LiteralExpression(literal="Use a professional tone."), + "sharedAiResults": ObjectExpression( + object={ + "summarize": RefExpression( + ref="/steps/summarize/output/result" + ) + } + ), + } + ), + ) +) +``` + +See [the Python SDK](../reference/sdk.md) for constructing the builder and +publishing through the normal APIs. No separate memory endpoint or provider SDK +is needed in your product. + +## Understand pauses, retries, and branches + +- **Pauses and recovery:** accepted outputs belong to the persisted execution, + rather than worker process memory. Restarting a worker does not erase them. +- **Replay:** Weave uses accepted evidence. Replaying a completed step does not + call its model again. +- **Ambiguous provider failures:** a provider may charge for a request even when + its response is lost. Shared context does not provide exactly-once model calls. + Review incidents through the normal recovery controls. +- **Branches:** a step can read only values available on its execution path. It + cannot read a future step or a concurrent sibling's incomplete result. Expose + branch outputs at the join when the next step needs them, then map those values + with the ordinary context editor. +- **Data boundaries:** choosing a result sends it to the consuming step's + provider, which may differ from the producing step's provider. Share only the + necessary fields. Normal schema, secret classification, and size limits still + apply; there is no bypass for AI context. diff --git a/docs/guides/standalone.md b/docs/guides/standalone.md index aa45b90b..c8f29f38 100644 --- a/docs/guides/standalone.md +++ b/docs/guides/standalone.md @@ -96,18 +96,18 @@ checkout. If you do not already have it, run: ```sh # Clone the alpha7 release that matches the CLI, then install its locked dependencies. -git clone --branch v0.1.0a10 --single-branch \ +git clone --branch v0.1.0a11 --single-branch \ https://github.com/fireflyframework/firefly-weave.git cd firefly-weave uv sync --locked --python 3.12 ``` -The clone selects the same **v0.1.0a10** release as the CLI installation guide. +The clone selects the same **v0.1.0a11** release as the CLI installation guide. A detached-HEAD message is expected when Git opens a release tag; this tutorial does not require creating a branch or editing application source. If you already have a checkout, enter its root and run `git describe --tags --exact-match`. -For this released walkthrough, the result must be `v0.1.0a10`. If the checkout has +For this released walkthrough, the result must be `v0.1.0a11`. If the checkout has another version or local development work, preserve it and clone the release into a separate directory by adding a new directory name to the clone command above. Contributors intentionally using unreleased source should use that checkout's diff --git a/docs/guides/studio-step-reference.md b/docs/guides/studio-step-reference.md index 8da55d8c..d44fc290 100644 --- a/docs/guides/studio-step-reference.md +++ b/docs/guides/studio-step-reference.md @@ -31,13 +31,13 @@ Studio writes the same versioned definition language that you can edit as YAML or build with the CLI and the Python SDK, so every choice here is visible in the **Source** tab. -**This page describes the Studio 0.1.0a10 editor.** The step kinds and native +**This page describes the Studio 0.1.0a11 editor.** The step kinds and native human tasks are also part of alpha6, but several controls described here, such as the canvas step picker, the searchable action picker, the **Value** / typed input rows with **Use data** and **Calculate…**, the schema designer, the API action builder, and the simulation setup and panel, are new in 0.1.0a7. An alpha6 or earlier browser bundle does not have them; to see the same screens, -[install the alpha10 browser application](studio.md#install-the-alpha10-browser-application). +[install the alpha11 browser application](studio.md#install-the-alpha11-browser-application). **How to use this page:** @@ -188,11 +188,32 @@ in the connected catalog. Select a **Workflow AI profile** and an **AI connectio slot**, then map **AI prompt** and **AI context** using Value, Data or Formula. Open **Configure workflow AI profiles** to create or edit a named profile. -Choose its provider and model explicitly, configure time and token limits, and -describe the expected result with the schema designer. Profile changes belong -to the workflow; select the profile on each AI task that should use it. Provider -credentials belong to an authorized connection, not the prompt or profile. -Workflow AI profiles are separate from Studio's Lumi assistant settings. +Studio alpha11 uses the same **Model → Connection → Review** structure as Lumi, +but saves only to this workflow. + +1. **Model:** choose the provider and model explicitly, configure time and token + limits, and describe the expected result with the schema designer. For Azure, + **Model** is the deployment name. Select **Continue to connection**. +2. **Connection:** choose the **AI connection slot**, or select **Add AI connection + slot** to include a new slot when you apply. An authorized administrator can + also open **New AI connection** to create an environment connection using an + operator-provisioned secret handle. Activation binds the slot to that + environment connection; this wizard does not activate the workflow. +3. **Review:** check the profile, provider, model, slot, and list of affected AI + steps. **Apply workflow AI settings** applies the profile, slot, and current + step's references together. **Back** keeps the draft; **Cancel profile + changes** discards unapplied profile edits. Existing named profiles affect + every step listed in the review. + +Use **Shared AI context** to select results from earlier AI steps available on +this path. Existing context stays under `data`; selected results appear under +`sharedAiResults`. This is structured data within one execution, not memory +shared between unrelated runs. The compiler enforces which prior results can be +read. See [Share context between AI steps](shared-ai-context.md) for a complete +example and the limits across branches, waits, and retries. + +Provider credentials belong to an authorized connection, not the prompt or +profile. Workflow AI profiles are separate from Studio's Lumi assistant settings. The [AI worker guide](ai-workers.md) covers deployment, supported providers, credential binding and execution limits. A configured inspector alone does not install the action or start a worker. diff --git a/docs/guides/studio.md b/docs/guides/studio.md index 6cf4fd72..0872d3a8 100644 --- a/docs/guides/studio.md +++ b/docs/guides/studio.md @@ -38,10 +38,10 @@ computer at `127.0.0.1`. That host keeps your saved platforms, which it shares with the `weave` command line, and keeps your sign-in in the operating system's credential store. The page never receives your tokens. -**This page describes Studio 0.1.0a10.** An alpha6 or earlier browser bundle +**This page describes Studio 0.1.0a11.** An alpha6 or earlier browser bundle shows an earlier connection assistant, which imports a login configuration file, and lacks the editor features and API actions described here. To follow this -page, [install the alpha10 browser application](#install-the-alpha10-browser-application). +page, [install the alpha11 browser application](#install-the-alpha11-browser-application). ![Studio and runtime boundaries](../diagrams/studio-and-runtime.svg) @@ -55,7 +55,7 @@ happens on the platform and keeps running after you close Studio. | Your goal | What must be running | Start here | | --- | --- | --- | | Open Studio as a native desktop app | A matching desktop build for your operating system | [Install desktop](desktop.md) | -| Draw, import, or validate a workflow on your computer | Studio's local host; no account | [Install Studio](#install-the-alpha10-browser-application), then [work locally](#work-locally-without-signing-in) | +| Draw, import, or validate a workflow on your computer | Studio's local host; no account | [Install Studio](#install-the-alpha11-browser-application), then [work locally](#work-locally-without-signing-in) | | Learn the editor with a small example | Studio's local host | [Try it: draw your first workflow](#try-it-draw-your-first-workflow) | | Save and run workflows on your laptop | Studio plus a separate local Weave platform | [Start the platform](local-platform.md), then [connect Studio](#connect-to-a-platform) | | Work with your team's platform | Studio on your computer and the server address from your administrator | [Connect to a platform](#connect-to-a-platform) | @@ -71,45 +71,45 @@ computer. You can create, import, edit, and validate workflow definitions. Connect when you want to save them to a shared platform, run processes, or handle tasks. This status does not mean your computer has lost its internet connection. -## Install the alpha10 browser application +## Install the alpha11 browser application Alpha7 includes the Studio host. Its browser assets are an optional, matching release bundle; the Python wheel does not embed the Angular application. Alpha4 does not include Studio. Download all files from the same -[v0.1.0a10 release](https://github.com/fireflyframework/firefly-weave/releases/tag/v0.1.0a10). +[v0.1.0a11 release](https://github.com/fireflyframework/firefly-weave/releases/tag/v0.1.0a11). The CLI installer does not automatically fetch or install the Studio ZIP. -Use Python 3.12 or newer. The alpha10 CLI installer includes the Studio host +Use Python 3.12 or newer. The alpha11 CLI installer includes the Studio host and authentication dependencies, but downloads no browser ZIP automatically. These Bash/Zsh commands need neither Node nor a source checkout: ```sh # Install the pinned CLI into its isolated installation directory. # Review the installer first using the download-and-inspect alternative in Installation. -curl --fail --location https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/install.sh \ - | sh -s -- --version v0.1.0a10 +curl --fail --location https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/install.sh \ + | sh -s -- --version v0.1.0a11 # Make the installed command available in this terminal. export PATH="$HOME/.local/bin:$PATH" # Check that the host version matches the browser bundle you will install. weave --version # Keep the downloaded optional browser bundle and its digest together. -mkdir weave-studio-alpha10 -cd weave-studio-alpha10 -curl --fail --location --remote-name https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/firefly-weave-studio-0.1.0a10.zip -curl --fail --location --remote-name https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/firefly-weave-studio-0.1.0a10.zip.sha256 +mkdir weave-studio-alpha11 +cd weave-studio-alpha11 +curl --fail --location --remote-name https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/firefly-weave-studio-0.1.0a11.zip +curl --fail --location --remote-name https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/firefly-weave-studio-0.1.0a11.zip.sha256 # Verify the bundle on macOS. Stop if the check fails. -shasum -a 256 --check firefly-weave-studio-0.1.0a10.zip.sha256 +shasum -a 256 --check firefly-weave-studio-0.1.0a11.zip.sha256 # On Linux, use sha256sum --check in place of shasum -a 256 --check. # Install only the bundle whose version and verified digest match this host. -weave studio install --bundle firefly-weave-studio-0.1.0a10.zip \ - --sha256 "$(awk '{print $1}' firefly-weave-studio-0.1.0a10.zip.sha256)" +weave studio install --bundle firefly-weave-studio-0.1.0a11.zip \ + --sha256 "$(awk '{print $1}' firefly-weave-studio-0.1.0a11.zip.sha256)" # Start the local host without opening a browser automatically. weave studio --no-browser ``` -Expected: `weave --version` prints `Firefly Weave 0.1.0a10`, the checksum line +Expected: `weave --version` prints `Firefly Weave 0.1.0a11`, the checksum line ends with `OK`, and the install prints `Studio installed at PATH. Start it with: weave studio`. The host then prints `Firefly Weave Studio · Local authoring (no platform selected)`, or `Firefly Weave Studio · Platform: NAME` when a saved @@ -131,7 +131,7 @@ or the [remote deployment guide](../operations/remote-deployment.md). **This route is for contributors,** and for trying changes made after the latest release; the release bundle above already has everything this page -describes. To reproduce the alpha10 release exactly, check out `v0.1.0a10` +describes. To reproduce the alpha11 release exactly, check out `v0.1.0a11` instead; development `main` can differ from released assets. From the repository root, install the development dependencies first. Node 24 @@ -167,7 +167,7 @@ while you use Studio. **Already installed a release browser bundle?** Installed bundles are kept per version, and a checkout can report the same version as the latest release, for -example `0.1.0a10`. A plain `weave studio` then keeps serving the installed +example `0.1.0a11`. A plain `weave studio` then keeps serving the installed release screens, and `weave studio install` keeps that bundle instead of replacing it. Always start a source build with `--assets studio/dist/studio/browser`. diff --git a/docs/installation.md b/docs/installation.md index 65743ebf..857a9324 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -27,18 +27,18 @@ The installer puts the CLI in its own Python environment, so your applications' packages are never affected. It includes local authoring, the API client with sign-in support, OpenAPI import, and the host that runs Studio. Studio's browser application is a separate download; see the -[Studio guide](guides/studio.md#install-the-alpha10-browser-application). +[Studio guide](guides/studio.md#install-the-alpha11-browser-application). ## Choose the installation path -**Recommended:** install the pinned **v0.1.0a10 alpha** below. You do not need Git, +**Recommended:** install the pinned **v0.1.0a11 alpha** below. You do not need Git, a source checkout, Docker, or `sudo`. An alpha is a preview release: use it to evaluate Weave, and check the [current limits](capabilities.md). | Your situation | Start here | | --- | --- | | You want to write, check, and simulate workflows, connect to your team's platform by its address (`weave auth setup`), or call REST APIs without code | [Install a release](#install-a-release), then [check the result](#check-the-result) | -| You want the visual editor in your browser | [Install Studio](guides/studio.md#install-the-alpha10-browser-application) | +| You want the visual editor in your browser | [Install Studio](guides/studio.md#install-the-alpha11-browser-application) | | You want a native desktop app | [Desktop installation](guides/desktop.md) | | You contribute code, or want to try changes made after the release | [Install the current source](#install-the-current-source) | | You already have the CLI | [Upgrade or select another version](#installation-locations-and-upgrades) | @@ -89,8 +89,8 @@ explicitly: # Stop if downloading the installer fails. set -o pipefail curl --proto '=https' --tlsv1.2 -fsSL \ - https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/install.sh \ - | sh -s -- --version v0.1.0a10 + https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/install.sh \ + | sh -s -- --version v0.1.0a11 ) ``` @@ -100,7 +100,7 @@ environment; and tests the new command before it makes it available. The first installation can take several minutes. No services start and no cloud resources are created. -Expected, at the end: `Installed Firefly Weave 0.1.0a10: PATH`, then `Run: PATH +Expected, at the end: `Installed Firefly Weave 0.1.0a11: PATH`, then `Run: PATH --help`, where `PATH` is the new command (by default `~/.local/bin/weave`). When that directory is not on your `PATH`, the installer also prints the `export` line to add. The last line says where earlier environments are kept for @@ -112,7 +112,7 @@ instead: ```sh # Download the installer to a file instead of running it directly. curl --proto '=https' --tlsv1.2 -fsSL \ - https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/install.sh \ + https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/install.sh \ -o weave-install.sh ``` @@ -121,7 +121,7 @@ editor, then run it: ```sh # Install the same pinned release with the reviewed file. -sh weave-install.sh --version v0.1.0a10 +sh weave-install.sh --version v0.1.0a11 ``` Expected: the same output as above. Keep the file if you want to reuse it for @@ -147,7 +147,7 @@ weave version --output json weave docs platform ``` -Expected: `Firefly Weave 0.1.0a10`; the help with its command groups and +Expected: `Firefly Weave 0.1.0a11`; the help with its command groups and examples; one JSON object with `version`, `apiVersion`, and `irVersion`; and the address of the platform guide. The Firefly banner appears only in the main help; it never appears in command results or JSON output. @@ -183,7 +183,7 @@ remove or move the Python interpreter the environment was built with. ### Select a version or release channel -The explicit `--version v0.1.0a10` makes an installation repeatable. To upgrade, +The explicit `--version v0.1.0a11` makes an installation repeatable. To upgrade, use the installer URL and `--version` value of the new release shown on [GitHub Releases](https://github.com/fireflyframework/firefly-weave/releases). Tags start with `v`; `weave --version` shows the package version without it. @@ -192,7 +192,7 @@ With a downloaded `weave-install.sh`, you can select a channel instead: | Command | What it installs | | --- | --- | -| `sh weave-install.sh --version v0.1.0a10` | Exactly this documented alpha | +| `sh weave-install.sh --version v0.1.0a11` | Exactly this documented alpha | | `sh weave-install.sh --prerelease` | The most recently published release, including alpha previews | | `sh weave-install.sh` | The latest stable release only | @@ -236,7 +236,7 @@ removal option: # Stop if downloading the installer fails. set -o pipefail curl --proto '=https' --tlsv1.2 -fsSL \ - https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a10/install.sh \ + https://github.com/fireflyframework/firefly-weave/releases/download/v0.1.0a11/install.sh \ | sh -s -- --uninstall ) ``` @@ -257,7 +257,7 @@ delete them yourself if you want no trace. | What you see | Why | What to do | | --- | --- | --- | | A Python version or `venv` error | The selected Python is older than 3.12, or lacks `venv` | Set `WEAVE_INSTALL_PYTHON` to Python 3.12 or newer; install your distribution's `venv` package | -| No installable release was found | You asked for the latest stable release, and only alphas exist | Run the pinned `v0.1.0a10` command above, or add `--prerelease` | +| No installable release was found | You asked for the latest stable release, and only alphas exist | Run the pinned `v0.1.0a11` command above, or add `--prerelease` | | Missing installer assets | The selected release is older than this installer | Choose `v0.1.0a2` or later; never mix files from different releases | | A checksum mismatch | A downloaded file is not the published one | Stop, and download the complete release again from the same trusted source | | The installer refuses to replace `weave` | Another program already uses that command path | Choose a different `--bin-dir`, or inspect the old command before replacing it yourself | @@ -268,7 +268,7 @@ delete them yourself if you want no trace. This route installs **unreleased development source from `main`**, for contributors and for trying changes made after the latest release. You do not -need it for any feature this documentation describes: the v0.1.0a10 release +need it for any feature this documentation describes: the v0.1.0a11 release includes them. Behavior on `main` can change before the next release. Follow the documentation of the same checkout. diff --git a/docs/operations/azure.md b/docs/operations/azure.md index ad7d231c..d0a1a3c0 100644 --- a/docs/operations/azure.md +++ b/docs/operations/azure.md @@ -162,9 +162,10 @@ row-level security. Never run the local fixture setup script there. See **Microsoft Entra ID as the identity provider for people is not verified.** Using Entra ID to manage access to AKS is separate from using it to sign people in to -Weave. The built-in token verifier accepts Microsoft's signing keys, which omit -`alg`, but no Entra token has been verified yet. Read -[Microsoft Entra ID (not verified)](identity-and-secrets.md#microsoft-entra-id-not-verified) +Weave. Azure preproduction checks have verified real Entra application tokens +for a host application and an independent worker. They do not verify interactive +human sign-in or qualify this AKS recipe. Read +[Microsoft Entra ID (human sign-in not verified)](identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) before you plan Weave sign-in with it. ## If something goes wrong diff --git a/docs/operations/identity-and-secrets.md b/docs/operations/identity-and-secrets.md index 95d727be..b7e7e388 100644 --- a/docs/operations/identity-and-secrets.md +++ b/docs/operations/identity-and-secrets.md @@ -290,10 +290,10 @@ The API adds the issuer of the `local-keycloak` provider, installation, and publishes `allow_loopback_http: true` because that provider is `local_development`. -**Microsoft Entra ID (illustrative, not verified).** This shape follows the -requirements in [Microsoft Entra ID](#microsoft-entra-id-not-verified). It has +**Microsoft Entra ID human sign-in (illustrative, not verified).** This shape follows the +requirements in [Microsoft Entra ID](#microsoft-entra-id-human-sign-in-not-verified). It has not been run against a Microsoft tenant. Replace every `YOUR_*` placeholder and -check the known verifier limitation in that section before you rely on it: +check the human sign-in requirements in that section before you rely on it: ```json [ @@ -586,7 +586,7 @@ are never used for authorization. | Provider | Server verification | Published sign-in | Status | | --- | --- | --- | --- | | Local Keycloak 26.7.4 from `weave platform setup` | Written to `runtime.env` | Written to `runtime.env`; display name set by `weave platform start` | Verified locally: browser sign-in with PKCE and the device flow, silent renewal with refresh-token rotation, revocation at sign-out, an unlinked account, and an account without grants | -| Microsoft Entra ID | Tenant-specific v2 profile | Illustrative entry in step 3 | Not verified; signing keys shaped like Entra's are covered by unit tests only, as described below | +| Microsoft Entra ID | Tenant-specific v2 profile | Illustrative human sign-in entry in step 3 | Application tokens verified in Azure preproduction for a host application and independent worker; human browser/device sign-in remains unverified | | Other OIDC providers | Profile built from the provider's access-token contract | Built from the checklist below | Not verified | ### Local Keycloak (development) @@ -650,16 +650,19 @@ realm. If browser sign-in is refused on a retained realm that `weave platform start` could not update, sign in with a code instead: `weave auth login --flow device`. -### Microsoft Entra ID (not verified) +### Microsoft Entra ID (human sign-in not verified) **Signing keys without `alg` are accepted by key type.** Microsoft's published signing keys for a tenant's v2 `jwks_uri` do not declare `alg`. The built-in verifier uses such an RSA key for `RS256` only, which is what Entra signs -access tokens with. Unit tests cover keys shaped like Entra's; no Entra token -has been verified yet. +access tokens with. Azure preproduction checks have verified real Entra +application tokens for a host application and an independent Agentic worker, +including the configured audience, client mapping, identity links, and scoped +grants. -The following requirements describe a configuration. None of them has been run -against a Microsoft tenant: +Application-token verification does not establish browser or device-code +sign-in for people. The following human sign-in configuration remains +unverified against a Microsoft tenant: 1. **Use the tenant-specific v2 issuer.** - The issuer is `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0`, and @@ -799,7 +802,7 @@ database login. | `ClaimsMapper` | Keycloak, generic, and Entra claim profiles; not wired into the built-in verifier | Exact provider claim shape and allowed mapping | | `PrincipalResolver` and identity links | Explicit local-principal mapping | Provisioning lifecycle and scope grants | | Published sign-in settings | Any provider listed in `WEAVE_OIDC_PROVIDERS` that meets the login-client checklist | Provider registration, scopes, and device verification origins | -| Entra and other CIAM products | Configuration guidance only | Not verified; Entra-shaped signing keys without `alg` are covered by unit tests only; no Entra group-overage expansion | +| Entra and other CIAM products | Entra application tokens verified in Azure preproduction; configuration guidance for human sign-in and other CIAM | Entra human browser/device sign-in and other providers remain unverified; no Entra group-overage expansion | An embedded verifier or resolver must keep these contracts. A generic mapper is not proof that a provider's ID token is safe as an API access token, or that its @@ -897,7 +900,7 @@ ones a person can fix alone. | `WV-AUTH-NOT-LINKED` | Sign-in worked, but the identity has no Weave principal yet | Link it with the details the person shares, as described in [People and access](../guides/people-and-access.md) | | `WV-AUTH-NO-ACCESS` | The principal is linked but has no grant in any environment | Grant a role in the intended workspace | | `WV-PROFILE-CHANGED` | The published issuer or login client changed after the person's review | The person reviews the platform again | -| HTTP 401 with `WV-UNAUTHENTICATED` for a token the provider issued | Issuer, audience, client mapping, token class, or signing key does not match the profile, or the identity is not linked | Compare the token's claims with step 2 and check the identity link; for Entra, see [Microsoft Entra ID (not verified)](#microsoft-entra-id-not-verified) | +| HTTP 401 with `WV-UNAUTHENTICATED` for a token the provider issued | Issuer, audience, client mapping, token class, or signing key does not match the profile, or the identity is not linked | Compare the token's claims with step 2 and check the identity link; for Entra, see [Microsoft Entra ID (human sign-in not verified)](#microsoft-entra-id-human-sign-in-not-verified) | | HTTP 403 with `WV-FORBIDDEN` | The identity is linked, but no grant covers that operation in that scope | Grant the role at the right level; an environment grant does not cover project-level operations such as retention and compatibility | **Changes take effect at different times.** A changed local grant applies at the diff --git a/docs/operations/remote-deployment.md b/docs/operations/remote-deployment.md index 29056dba..a243378e 100644 --- a/docs/operations/remote-deployment.md +++ b/docs/operations/remote-deployment.md @@ -33,9 +33,9 @@ is deployed with checked-in Kubernetes manifests. Running `weave` on your laptop creates no cloud cluster: your cloud's infrastructure tooling supplies the cluster, registry, database, and network. -**Planning Microsoft Entra ID for sign-in?** It has configuration guidance but -has not been verified yet. -[Read the requirements](identity-and-secrets.md#microsoft-entra-id-not-verified) +**Planning Microsoft Entra ID for people?** Application tokens have been +verified in Azure preproduction; human browser and device-code sign-in have not. +[Read the requirements](identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) before you choose your identity provider. ## Before you open a cloud terminal diff --git a/docs/operations/upgrades.md b/docs/operations/upgrades.md index 1416df51..15c42729 100644 --- a/docs/operations/upgrades.md +++ b/docs/operations/upgrades.md @@ -40,6 +40,7 @@ the desktop app never migrates a platform. | alpha8 | `0028_files` | Adds decision-table artifacts, separate Lumi configuration, file metadata/chunks/retention, and new scoped file and assistant roles. Existing grants do not automatically gain these roles. | | alpha9 | `0028_files` | No new schema migration. Adds guided AI configuration and renewable worker OAuth2 authentication. | | alpha10 | `0028_files` | No new schema migration. Restores Lumi access through the Studio host; explicit selection supersedes pending canvas fitting, and initial empty fields no longer steal focus. | +| alpha11 | `0028_files` | Studio AI setup wizards and explicit same-execution shared AI context; no server behavior or schema change. Updating the local Studio host and assets does not require redeploying an alpha10 server. | ![Schema, compatibility and execution acceptance gates](../diagrams/operations-upgrade.svg) @@ -264,8 +265,9 @@ the schema stays at `0025_run_lifecycle`. After the upgrade, check these points: verifier uses a JWKS key that omits `alg`, as Microsoft Entra ID publishes them, only for the allowed algorithm of its key type: an RSA key for `RS256`, a P-256 key for `ES256`. Keys that declare `alg` are checked as before. Unit - tests cover Entra-shaped keys; no Entra token has been verified, as - [Microsoft Entra ID (not verified)](identity-and-secrets.md#microsoft-entra-id-not-verified) + tests cover Entra-shaped keys, and Azure preproduction checks have verified + real Entra application tokens. Human sign-in remains unverified, as + [Microsoft Entra ID](identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) explains. - **Local Keycloak realms accept any loopback port.** A local platform still has [no in-place upgrade](#a-local-platform-has-no-in-place-upgrade), so set up diff --git a/docs/reference/local-runtime.md b/docs/reference/local-runtime.md index 2881cde7..c8641cdb 100644 --- a/docs/reference/local-runtime.md +++ b/docs/reference/local-runtime.md @@ -248,8 +248,10 @@ through Keycloak administration and link it with the [people and access commands](../guides/people-and-access.md). Signing in with the local Keycloak was verified against the real Keycloak 26.7.4 -sign-in pages, with PKCE and with the device code flow. Other identity providers, -such as Microsoft Entra ID, need their own configuration and are not verified; +sign-in pages, with PKCE and with the device code flow. Microsoft Entra ID +application tokens have separately been verified in Azure preproduction; Entra +browser and device-code sign-in for people remain unverified. Other identity +providers need their own configuration and acceptance checks; see [Use your own identity provider](../operations/identity-and-secrets.md#use-your-own-identity-provider). ## Connector actions on a local platform @@ -297,8 +299,9 @@ published keys (JWKS). snapshots; resolve again for a new operation. Other providers need a matching trust profile; -[Microsoft Entra ID](../operations/identity-and-secrets.md#microsoft-entra-id-not-verified) -is described there and is not verified. +[Microsoft Entra ID](../operations/identity-and-secrets.md#microsoft-entra-id-human-sign-in-not-verified) +application tokens have been verified in Azure preproduction; human sign-in +still needs its own acceptance checks. ## Tenant isolation and compatibility inventory @@ -408,7 +411,7 @@ supervision, none of which the local setup scripts provision. The locked framework is the published **PyFly 26.9.15**; the exact wheel hash and upstream commit are in the [project metadata](../../pyproject.toml). The API -package version is `0.1.0a10`; a checkout of `main` can carry unreleased changes +package version is `0.1.0a11`; a checkout of `main` can carry unreleased changes on top of it. Validate readiness, an authorized workflow, and your [backup and restore procedure](../operations/backup-restore.md) in the environment you intend to operate. diff --git a/docs/reference/native-openapi.md b/docs/reference/native-openapi.md index 8dbbca27..8a2567fc 100644 --- a/docs/reference/native-openapi.md +++ b/docs/reference/native-openapi.md @@ -118,7 +118,7 @@ each of these. ## Versions and what the document proves - The document's `info.version` is the API version, `1`. It is separate from - the package version (`0.1.0a10`) and from the workflow language version + the package version (`0.1.0a11`) and from the workflow language version (`weave/v1alpha1`). - The exporter uses PyFly 26.9.15. The exact URL, SHA-256, and upstream provenance are in [project metadata](../../pyproject.toml) and the lockfile. diff --git a/mkdocs.yml b/mkdocs.yml index 853b8123..247dae3f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -110,6 +110,7 @@ nav: - Connect FTP and cloud drives: guides/file-connectors.md - Evaluate decision tables: reference/decision-tables.md - Run AI steps: guides/ai-workers.md + - Share context between AI steps: guides/shared-ai-context.md - Configure Lumi assistance: guides/lumi.md - Manage runs and business cases: guides/execution-management.md - Resolve incidents, cancel, and retry: reference/incident-operations.md diff --git a/pyproject.toml b/pyproject.toml index 7fd6c526..3c41c35d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ build-backend = "hatchling.build" [project] name = "firefly-weave" -version = "0.1.0a10" +version = "0.1.0a11" description = "API-first workflow orchestration and integration platform built on PyFly" readme = "README.md" license = "Apache-2.0" diff --git a/src/firefly_weave/__init__.py b/src/firefly_weave/__init__.py index bf3ddab3..e9294f7d 100644 --- a/src/firefly_weave/__init__.py +++ b/src/firefly_weave/__init__.py @@ -16,4 +16,4 @@ """Firefly Weave's offline contracts and workflow tooling.""" -__version__ = "0.1.0a10" +__version__ = "0.1.0a11" diff --git a/studio/src/app/designer/llm-inspector.ts b/studio/src/app/designer/llm-inspector.ts index 56dcd0c4..904b9cbc 100644 --- a/studio/src/app/designer/llm-inspector.ts +++ b/studio/src/app/designer/llm-inspector.ts @@ -30,7 +30,9 @@ import { ExpressionEditor } from "../property-grid"; import { aiConnector } from "../integrations/ai-provider-connection"; import { compatibleSlots, slotName } from "../integrations/slot-binding"; import { Select } from "../forms/ui/select"; -import { TaskForm } from "../task-form"; +import { AiProfileEditor } from "../integrations/ai-profile-editor"; +import { AiSetupWizard } from "../integrations/ai-setup-wizard"; +import { AiSharedContextPicker } from "../integrations/ai-shared-context-picker"; import { SchemaDesigner } from "../forms/ui/schema-designer"; import { referencesAt } from "../forms/core/reference-context"; import { describeError } from "../errors"; @@ -49,7 +51,14 @@ const object = (value: unknown): RecordValue => selector: "weave-llm-inspector", standalone: true, changeDetection: ChangeDetectionStrategy.Eager, - imports: [ExpressionEditor, TaskForm, SchemaDesigner, Select], + imports: [ + ExpressionEditor, + AiProfileEditor, + AiSetupWizard, + AiSharedContextPicker, + SchemaDesigner, + Select, + ], template: `
fields.

} -
- -
- @if (!aiSlots.length) { -

- Add an AI connection slot for this workflow. Activation binds it to an - approved provider connection in the environment. -

- } - - @if (host.profile && host.can("connection.manage")) { - - }

Workflow profiles do not change Lumi. A platform operator must install and authorize the Agentic worker before these tasks can run.

-
/>
+ > Configure workflow AI profiles

- All steps that choose a profile share its model, reasoning and - generation limits. For Azure, Model is the deployment name. The - endpoint, API version and approved secret handle belong to the - environment connection. + Profile changes stay in this draft until you review and apply them. + All steps using the same named profile share its settings.

(choose)="loadProfile($event)" /> - @if (schema) { -
- -
- - @if (!profileValid || !profileName) { -

- Complete the required profile fields to save this profile. -

- } - @if (profileValid && existingProfile) { -

- Valid profile changes update the workflow automatically. -

- } + +
+ + @if (!nameValid) { +

+ Choose a unique profile name using letters, numbers, dots, + underscores or hyphens. +

+ } + + +
+
+
+ +
+

+ Activation binds this slot to an approved provider connection in + each environment. Provider credentials are never stored in this + workflow. +

+ @if (!aiSlots.length && !newSlot) { +

Add an AI connection slot to continue.

+ } + @if (newSlot) { +

+ New slot {{ newSlot }} will be added when you apply these + settings. +

+ } + + @if (host.profile && host.can("connection.manage")) { + + } + +
+
+
+
Profile
+
{{ profileName }}
+
Provider
+
{{ profile["provider"] }}
+
Model or Azure deployment
+
{{ profile["model"] }}
+
Connection slot
+
{{ draftSlot }}
+
+

+ Activation binds this slot to the environment connection; this + edit does not activate or run the workflow. +

+

These steps will use this profile:

+
    + @for (id of affectedSteps; track id) { +
  • {{ id }}
  • + } +
+ @if (affectedSteps.length > 1) { +

+ Applying changes updates the shared model, reasoning, + generation limits and result fields for every listed step. +

+ } +
+ +
} @else if (loading) {

Loading profile fields…

} @else { @@ -202,6 +264,9 @@ const object = (value: unknown): RecordValue => } + @if (applied) { +

Workflow AI settings applied.

+ } @if (error) { } @@ -231,6 +296,13 @@ const object = (value: unknown): RecordValue => min-width: 0; max-width: 100%; } + dd { + margin: 0 0 12px; + overflow-wrap: anywhere; + } + dt { + font-weight: 600; + } details[open] > summary { margin-bottom: 12px; } @@ -254,6 +326,11 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { profilesOpen = false; profileValid = false; resultValid = true; + wizardStep = 0; + draftSlot = ""; + newSlot = ""; + applied = false; + private loadedProfile = ""; private invalid = new Set(); get step() { return JSON.parse(this.host.inspectorBuffer) as Step; @@ -262,7 +339,10 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { return this.profileNames.map((name) => ({ value: name, label: name })); } get slotOptions() { - return this.aiSlots.map((slot) => ({ + const slots = this.newSlot + ? [...this.aiSlots, { name: this.newSlot, connector: aiConnector }] + : this.aiSlots; + return slots.map((slot) => ({ value: slot.name, label: slot.name, description: slot.connector, @@ -272,14 +352,48 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { return compatibleSlots({ connector: aiConnector }, this.host.workflowSlots); } addAiSlot() { - this.host.newSlotName = slotName( + this.newSlot = slotName( "ai", this.host.workflowSlots.map((slot) => slot.name), ); - this.host.newSlotNameEdited = true; - this.host.newSlotConnector = aiConnector; - this.host.newSlotRequired = true; - this.host.addConnectionSlot(); + this.draftSlot = this.newSlot; + } + get nameValid() { + return ( + /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(this.profileName) && + (!this.profiles[this.profileName] || + this.profileName === this.loadedProfile) + ); + } + get completeProfile() { + return ( + this.nameValid && + this.profileValid && + this.resultValid && + !!this.schema && + missingData(this.schema, this.profile).length === 0 + ); + } + get validSlot() { + return ( + this.aiSlots.some((slot) => slot.name === this.draftSlot) || + (!!this.newSlot && + this.draftSlot === this.newSlot && + !this.host.workflowSlots.some((slot) => slot.name === this.newSlot)) + ); + } + get canContinue() { + return this.completeProfile && (this.wizardStep === 0 || this.validSlot); + } + get affectedSteps() { + return this.host.model + .nodes() + .filter( + ({ step }) => + step.kind === "llm" && + (step["profile"] === this.profileName || step.id === this.step.id), + ) + .map(({ step }) => step.id); } get profiles() { return object(this.host.model.definition.spec["llmProfiles"]); @@ -340,6 +454,12 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { } loadProfile(name: string) { this.profileName = name; + this.loadedProfile = name; + this.wizardStep = 0; + this.draftSlot = String(this.step["connection"] ?? ""); + this.newSlot = ""; + this.applied = false; + this.resultValid = true; this.existingProfile = !!this.profiles[name]; if (!this.existingProfile) this.profilesOpen = true; const loaded = object(this.profiles[name]); @@ -349,7 +469,8 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { const { outputSchema: _, ...rest } = loaded; this.initialProfile = structuredClone(rest); this.profile = structuredClone(rest); - this.profileValid = false; + // TaskForm clears field errors on reload without emitting initial validity. + this.profileValid = true; } newProfile() { let name = "default"; @@ -357,34 +478,47 @@ export class LlmInspector implements OnInit, DoCheck, OnDestroy { while (this.profiles[name]) name = `profile-${i++}`; this.loadProfile(name); } - profileData(value: RecordValue) { - this.profile = value; - this.saveProfile(); - } - profileValidity(valid: boolean) { - this.profileValid = valid; - if (!valid || !this.resultValid) - this.host.inspectorFieldValidity({ - path: "spec/llmProfiles", - valid: false, - }); + cancelProfile() { + this.loadProfile(String(this.step["profile"] ?? "")); + this.profilesOpen = false; } saveProfile() { if ( - !this.profileValid || - !this.schema || - missingData(this.schema, this.profile).length > 0 || - !this.resultValid || - !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(this.profileName) + !this.completeProfile || + !this.validSlot || + this.host.model.readonly || + this.host.editingLocked ) return; + this.host.flushInspector(); const document = structuredClone(this.host.model.definition); document.spec["llmProfiles"] = { ...this.profiles, - [this.profileName]: { ...this.profile, outputSchema: this.resultSchema }, + [this.profileName]: structuredClone({ + ...this.profile, + outputSchema: this.resultSchema, + }), }; - this.host.workflowEdit({ value: document, path: "spec/llmProfiles" }); + if (this.newSlot && this.draftSlot === this.newSlot) { + document.spec["connections"] = { + ...object(document.spec["connections"]), + [this.newSlot]: { connector: aiConnector, required: true }, + }; + } + this.host.model.batch(() => { + this.host.workflowEdit({ value: document, path: "spec/llmProfiles" }); + this.host.flushInspector(); + this.host.stepEdit( + { ...this.step, profile: this.profileName, connection: this.draftSlot }, + "profile", + ); + this.host.flushInspector(); + }); this.existingProfile = true; + this.loadedProfile = this.profileName; + this.newSlot = ""; + this.profilesOpen = false; + this.applied = true; } async loadSchema() { this.loading = true; diff --git a/studio/src/app/integrations/ai-profile-editor.ts b/studio/src/app/integrations/ai-profile-editor.ts new file mode 100644 index 00000000..a88c66c1 --- /dev/null +++ b/studio/src/app/integrations/ai-profile-editor.ts @@ -0,0 +1,56 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import { Component, input, output } from "@angular/core"; +import { TaskForm } from "../task-form"; +import type { Schema } from "../task-schema"; + +/** Uses the host's canonical contract for both workflow and assistant profiles. */ +@Component({ + selector: "weave-ai-profile-editor", + standalone: true, + imports: [TaskForm], + template: ` +

+ Choose a provider and model. For Azure OpenAI, enter the deployment name. + Generation and reasoning limits apply to each AI request; credentials + belong to the connection in the next step. +

+ + `, + styles: ` + :host { + display: grid; + gap: 12px; + min-width: 0; + } + .hint { + margin: 0; + } + `, +}) +export class AiProfileEditor { + schema = input.required(); + initialData = input.required>(); + dataChange = output>(); + validityChange = output(); +} diff --git a/studio/src/app/integrations/ai-provider-connection-form.ts b/studio/src/app/integrations/ai-provider-connection-form.ts index 0c02ce9f..ccaaf462 100644 --- a/studio/src/app/integrations/ai-provider-connection-form.ts +++ b/studio/src/app/integrations/ai-provider-connection-form.ts @@ -19,6 +19,9 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ElementRef, + Injector, + afterNextRender, OnDestroy, OnInit, inject, @@ -43,18 +46,13 @@ import { changeDetection: ChangeDetectionStrategy.Eager, imports: [Select], template: ` -

- Choose the provider's approved endpoint and stored API key handle for this - environment. Model or Azure deployment names belong in workflow AI - profiles or Lumi settings. -

@if (!canManage()) {

Ask an administrator for connection.manage in this environment to create a provider connection.

} @else if (done) { -

Connection created

+

Connection created

{{ done.name }} · revision {{ done.revision }}

The platform checked this configuration. No model request was sent. A @@ -64,6 +62,17 @@ import {

} @else { +
    + @for (label of stages; track label; let index = $index) { +
  1. + + {{ label }} +
  2. + } +
@if (loading) {

Checking the published AI connector…

} @else if (!connectorId) { @@ -75,77 +84,200 @@ import { }
- - - -

- Only this endpoint's exact HTTPS origin is allowed. The worker and - Lumi gateway must also allow it. -

- @if (draft.provider.startsWith("azure-")) { + @if (step === 0) { +

Choose a provider

+

+ Name this connection so authors can recognize it. Choose models or + Azure deployments later in workflow AI profiles or Lumi settings. +

+ >Connection name + + + + } @else if (step === 1) { +

+ Endpoint and secret handle +

+

+ Use the operator-approved endpoint and secret handle for + {{ providerLabel }} in this environment. +

+ +

+ Copy the exact base endpoint approved by your operator. The worker + or Lumi gateway must allow this endpoint; this connection permits + only its HTTPS origin. +

+ @if (draft.provider.startsWith("azure-")) { + +

+ Use the API version approved for your deployment. +

+ } + +

+ Enter the handle supplied by your platform operator. The operator + stores and grants the secret for this environment. Never paste the + API key. +

+ } @else { +

Review your connection

+

+ Check these details before creating an immutable connection + revision. +

+
+
+
Connection name
+
{{ draft.name.trim() }}
+
+
+
Provider
+
{{ providerLabel }}
+
+
+
Endpoint
+
{{ draft.endpoint.trim() }}
+
+ @if (draft.provider.startsWith("azure-")) { +
+
Azure API version
+
{{ draft.apiVersion.trim() }}
+
+ } +
+
API key secret handle
+
{{ draft.handle.trim() }}
+
+
+

+ The platform will validate the configuration. No model request or + connectivity test is sent. Your operator still needs to authorize + the worker release or configure Lumi's gateway before use. +

} - -

- Enter the handle supplied by your platform operator. The operator - stores and grants the secret for this environment. Never paste the - API key. -

- +
+ @if (step > 0) { + + } + +
} @if (error) { - + } `, styles: ` :host { display: grid; - gap: 12px; + gap: 16px; min-width: 0; } + .progress { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + list-style: none; + padding: 0 0 16px; + margin: 0; + border-bottom: 1px solid var(--line); + } + .progress li { + display: flex; + align-items: center; + gap: 6px; + color: var(--muted); + font-size: 13px; + } + .progress li[aria-current] { + color: var(--forest); + font-weight: 650; + } + .number { + display: grid; + place-items: center; + width: 24px; + height: 24px; + flex: 0 0 24px; + border: 1px solid var(--field-border); + border-radius: 50%; + } + [aria-current] .number { + background: var(--forest); + border-color: var(--forest); + color: var(--on-dark); + } + .complete .number { + background: var(--selected); + color: var(--forest); + } fieldset { display: grid; - gap: 12px; + gap: 14px; border: 0; padding: 0; margin: 0; min-width: 0; } + .wizard-title { + margin: 0; + font-size: 17px; + line-height: 1.4; + } label { display: grid; gap: 6px; @@ -155,15 +287,58 @@ import { width: 100%; box-sizing: border-box; } - .hint, p { margin: 0; + overflow-wrap: anywhere; } - button { - justify-self: start; + .actions { + display: flex; + justify-content: flex-end; + flex-wrap: wrap; + gap: 8px; + border-top: 1px solid var(--line); + margin-top: 4px; + padding-top: 16px; } - p { + .actions button { + min-height: 40px; + } + .actions button:first-child:not(.primary) { + margin-right: auto; + } + .review { + margin: 0; + display: grid; + gap: 12px; + min-width: 0; + } + .review div { + display: grid; + grid-template-columns: minmax(100px, 1fr) minmax(0, 1.5fr); + gap: 12px; + } + dt { + color: var(--muted); + } + dd { + margin: 0; overflow-wrap: anywhere; + font-weight: 550; + } + .review-note { + border-left: 3px solid var(--jade); + padding: 10px 12px; + background: var(--mist); + } + @media (max-width: 420px) { + .progress li { + flex-direction: column; + align-items: flex-start; + } + .review div { + grid-template-columns: minmax(0, 1fr); + gap: 3px; + } } `, }) @@ -181,6 +356,21 @@ export class AiProviderConnectionForm implements OnInit, OnDestroy { apiVersion: "", handle: "", }; + readonly stages = ["Provider", "Access", "Review"]; + step = 0; + get providerLabel() { + return ( + this.providers.find((provider) => provider.value === this.draft.provider) + ?.label ?? "your provider" + ); + } + get endpointHint() { + return this.draft.provider.startsWith("azure-") + ? "https://your-resource.openai.azure.com" + : this.draft.provider === "anthropic" + ? "https://api.anthropic.com" + : "https://api.openai.com/v1"; + } connectorId = ""; loading = false; busy = false; @@ -188,6 +378,24 @@ export class AiProviderConnectionForm implements OnInit, OnDestroy { done: AiConnection | null = null; private alive = true; private cdr = inject(ChangeDetectorRef); + private host = inject>(ElementRef); + private injector = inject(Injector); + private focus(selector = ".wizard-title") { + const step = this.step; + afterNextRender( + () => { + if (this.alive && this.step === step) + this.host.nativeElement.querySelector(selector)?.focus(); + }, + { injector: this.injector }, + ); + } + back() { + if (this.busy || this.loading || this.step === 0) return; + this.step--; + this.error = ""; + this.focus(); + } private attempt: { body: ReturnType; key: string; @@ -230,10 +438,31 @@ export class AiProviderConnectionForm implements OnInit, OnDestroy { } async submit(event: Event) { event.preventDefault(); - if (this.busy || !this.canManage()) return; + if (this.busy || this.loading || !this.canManage() || !this.connectorId) + return; this.error = ""; try { + if (this.step === 0) { + if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(this.draft.name.trim())) + throw Error( + "Enter a connection name using letters, numbers, dots, dashes or underscores.", + ); + if ( + !this.providers.some( + (provider) => provider.value === this.draft.provider, + ) + ) + throw Error("Choose a provider."); + this.step = 1; + this.focus(); + return; + } const body = aiConnectionRequest(this.draft, this.connectorId); + if (this.step === 1) { + this.step = 2; + this.focus(); + return; + } if ( !this.attempt || JSON.stringify(this.attempt.body) !== JSON.stringify(body) @@ -250,9 +479,11 @@ export class AiProviderConnectionForm implements OnInit, OnDestroy { if (!this.alive) return; this.done = result; this.created.emit(result); + this.focus(); } catch (error) { if (this.alive) { this.error = describeError(error).message; + this.focus(".error"); if ( error instanceof ApiError && error.code === "WV-IDEMPOTENCY-CONFLICT" diff --git a/studio/src/app/integrations/ai-setup-wizard.ts b/studio/src/app/integrations/ai-setup-wizard.ts new file mode 100644 index 00000000..7b47e0bd --- /dev/null +++ b/studio/src/app/integrations/ai-setup-wizard.ts @@ -0,0 +1,192 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import { + ChangeDetectorRef, + Component, + ElementRef, + Input, + inject, + output, +} from "@angular/core"; + +/** Shared navigation only; each caller retains its draft and save authority. */ +@Component({ + selector: "weave-ai-setup-wizard", + standalone: true, + template: ` +
    + @for (label of labels; track $index) { +
  1. + {{ label }} +
  2. + } +
+

{{ headings[step] }}

+ +
+
+
+ +
+ @if (step > 0) { + + } + @if (step < 2) { + + } @else { + + } +
+ + `, + styles: ` + :host { + display: grid; + gap: 16px; + min-width: 0; + } + [hidden] { + display: none !important; + } + .wizard-actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + padding-top: 16px; + border-top: 1px solid var(--line); + } + .settings-progress { + display: flex; + gap: 16px; + padding: 0 0 16px; + margin: 0; + list-style: none; + border-bottom: 1px solid var(--line); + flex-wrap: wrap; + } + .settings-progress li { + display: flex; + align-items: center; + gap: 8px; + color: var(--muted); + } + .settings-progress [aria-current] { + color: var(--forest); + font-weight: 650; + } + .step-number { + display: grid; + place-items: center; + width: 28px; + height: 28px; + border-radius: 50%; + border: 1px solid var(--field-border); + } + [aria-current] .step-number { + color: var(--on-dark); + background: var(--forest); + border-color: var(--forest); + } + .complete .step-number { + background: var(--selected); + } + .settings-heading { + margin: 0; + } + + @media (max-width: 480px) { + .settings-progress { + gap: 12px; + font-size: 12px; + } + .settings-progress li { + gap: 4px; + } + .step-number { + width: 22px; + height: 22px; + } + } + `, +}) +export class AiSetupWizard { + @Input() step = 0; + @Input() labels = ["Model", "Connection", "Review"]; + @Input() headings = [ + "Choose a model", + "Choose a connection", + "Review AI settings", + ]; + @Input() nextLabels = ["Continue to connection", "Review settings"]; + @Input() finishLabel = "Apply settings"; + @Input() progressLabel = "AI setup progress"; + @Input() canContinue = false; + @Input() busy = false; + @Input() navigationBlocked = false; + stepChange = output(); + finish = output(); + private cdr = inject(ChangeDetectorRef); + private element = inject>(ElementRef); + move(direction: -1 | 1) { + if ( + this.busy || + this.navigationBlocked || + (direction === 1 && !this.canContinue) + ) + return; + this.step = Math.max(0, Math.min(2, this.step + direction)); + this.stepChange.emit(this.step); + this.cdr.detectChanges(); + this.element.nativeElement + .querySelector(".settings-heading") + ?.focus(); + } + complete() { + if ( + this.step === 2 && + this.canContinue && + !this.busy && + !this.navigationBlocked + ) + this.finish.emit(); + } +} diff --git a/studio/src/app/integrations/ai-shared-context-picker.ts b/studio/src/app/integrations/ai-shared-context-picker.ts new file mode 100644 index 00000000..9b380340 --- /dev/null +++ b/studio/src/app/integrations/ai-shared-context-picker.ts @@ -0,0 +1,149 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import { Component, input, output } from "@angular/core"; +import type { ScopeEntry } from "../forms/core/scope"; +import { + availableAiResults, + sharedAiResults, + withSharedAiResults, +} from "./ai-shared-context"; + +@Component({ + selector: "weave-ai-shared-context-picker", + standalone: true, + template: ` +
+ Shared AI context +

+ Choose earlier AI results this step can read. They stay within this + execution and survive pauses and retries. Your existing context is kept + under data; selected results appear under sharedAiResults. +

+ @for (ref of choices; track ref.ref) { + + } @empty { +

+ No earlier AI result is available here. Add an AI step earlier in this + path, or use the context editor below for workflow inputs and other + results. +

+ } + @for (id of unavailable; track id) { +

+ The result from {{ id }} is no longer available before this step. +

+ } + @if (unavailable.length) { + + } +

+ Only selected structured results are sent to this step's provider. + Conversations from Lumi and other executions are never included. +

+ @if (error) { + + } +
+ `, + styles: ` + :host { + display: block; + min-width: 0; + } + fieldset { + display: grid; + gap: 10px; + border: 1px solid var(--line); + border-radius: 8px; + padding: 12px; + margin: 0; + min-width: 0; + } + legend { + font-weight: 600; + padding: 0 4px; + } + label { + display: flex; + align-items: center; + gap: 8px; + overflow-wrap: anywhere; + } + input { + flex: 0 0 auto; + } + p { + margin: 0; + overflow-wrap: anywhere; + } + `, +}) +export class AiSharedContextPicker { + value = input.required(); + references = input([]); + valueChange = output(); + error = ""; + get choices() { + return availableAiResults(this.references()); + } + get selected() { + return sharedAiResults(this.value()); + } + get unavailable() { + return this.selected.filter( + (id) => !this.choices.some((ref) => ref.stepId === id), + ); + } + toggle(id: string, event: Event) { + const checked = (event.target as HTMLInputElement).checked; + if ( + !this.update( + checked + ? [...this.selected, id] + : this.selected.filter((item) => item !== id), + ) + ) + (event.target as HTMLInputElement).checked = this.selected.includes(id); + } + removeUnavailable() { + this.update(this.selected.filter((id) => !this.unavailable.includes(id))); + } + private update(selected: string[]) { + try { + this.valueChange.emit( + withSharedAiResults(this.value(), selected, this.references()), + ); + this.error = ""; + return true; + } catch (error) { + this.error = + error instanceof Error + ? error.message + : "Could not update shared context."; + return false; + } + } +} diff --git a/studio/src/app/integrations/ai-shared-context.ts b/studio/src/app/integrations/ai-shared-context.ts new file mode 100644 index 00000000..e486d568 --- /dev/null +++ b/studio/src/app/integrations/ai-shared-context.ts @@ -0,0 +1,101 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import type { ScopeEntry } from "../forms/core/scope"; + +const record = (value: unknown): value is Record => + !!value && typeof value === "object" && !Array.isArray(value); + +/** Only compiler-visible complete AI results; no future or sibling branch reads. */ +export function availableAiResults(references: ScopeEntry[]): ScopeEntry[] { + return references.filter( + (ref) => + ref.stepKind === "llm" && + !!ref.stepId && + !ref.optional && + ref.path.length === 1 && + ref.path[0] === "result", + ); +} + +function envelope(value: unknown): Record | null { + if ( + !record(value) || + Object.keys(value).length !== 1 || + !record(value["object"]) + ) + return null; + const fields = value["object"]; + const results = fields["sharedAiResults"]; + if ( + Object.keys(fields).length !== 2 || + !("data" in fields) || + !record(results) || + Object.keys(results).length !== 1 || + !record(results["object"]) + ) + return null; + for (const [id, ref] of Object.entries(results["object"])) { + if ( + !record(ref) || + Object.keys(ref).length !== 1 || + ref["ref"] !== + `/steps/${id.replace(/~/g, "~0").replace(/\//g, "~1")}/output/result` + ) + return null; + } + return fields; +} +export function sharedAiResults(value: unknown): string[] { + const fields = envelope(value); + return fields + ? Object.keys( + (fields["sharedAiResults"] as { object: Record }) + .object, + ) + : []; +} + +/** Build ordinary expressions so persistence, scope and replay keep one authority. */ +export function withSharedAiResults( + value: unknown, + selected: string[], + references: ScopeEntry[], +): unknown { + if (selected.length > 16) throw Error("Choose up to 16 prior AI results."); + if (new Set(selected).size !== selected.length) + throw Error("Choose each AI result only once."); + const allowed = new Map( + availableAiResults(references).map((ref) => [ref.stepId!, ref.ref]), + ); + if (selected.some((id) => !allowed.has(id))) + throw Error( + "Choose only AI results available before this step. Remove unavailable results first.", + ); + const data = envelope(value)?.["data"] ?? value; + if (!selected.length) return data; + return { + object: { + data, + sharedAiResults: { + object: Object.fromEntries( + selected.map((id) => [id, { ref: allowed.get(id)! }]), + ), + }, + }, + }; +} diff --git a/studio/src/app/lumi/lumi-panel.ts b/studio/src/app/lumi/lumi-panel.ts index 859223c6..e0c500c2 100644 --- a/studio/src/app/lumi/lumi-panel.ts +++ b/studio/src/app/lumi/lumi-panel.ts @@ -35,7 +35,8 @@ import { type AiConnection, } from "../integrations/ai-provider-connection"; import { Modal } from "../dialog"; -import { TaskForm } from "../task-form"; +import { AiProfileEditor } from "../integrations/ai-profile-editor"; +import { AiSetupWizard } from "../integrations/ai-setup-wizard"; import type { Schema } from "../task-schema"; import { missingData } from "../forms/core/form-model"; import { describeError } from "../errors"; @@ -57,7 +58,13 @@ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; selector: "weave-lumi-panel", standalone: true, changeDetection: ChangeDetectionStrategy.Eager, - imports: [Modal, TaskForm, Select, AiProviderConnectionForm], + imports: [ + Modal, + AiProfileEditor, + AiSetupWizard, + Select, + AiProviderConnectionForm, + ], template: ` @if (host.lumiOpen) { {{ status.provider }} · {{ status.model }} } - @if (host.profile && host.can("lumi.manage")) { - } @@ -100,7 +115,6 @@ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; } @if (settings) {
-

Lumi model and connection

These settings apply only to this assistant in this environment. Workflow AI profiles are configured separately. @@ -113,79 +127,125 @@ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

} @if (configSchema) { - - -

- Lumi pins this exact revision. Choose a connection with the same - provider as the profile; changing a workflow profile will not - change these settings. -

- @if (configConnection && !selectedConnection) { -

- Choose an available connection that matches this provider. -

- } - @if (host.can("connection.manage")) { - @if (!connectionOptions.length) { -

- No matching provider connections yet. Create one with an - operator-approved secret handle, or change the provider. + +

+ +
+
+

+ Select where Lumi sends requests. Your platform operator + supplies the approved endpoint and stores the credentials.

- } -
+ +

+ Lumi pins this exact revision. Choose a connection with the + same provider as the profile; changing a workflow profile + will not change these settings. +

+ @if (configConnection && !selectedConnection) { +

+ Choose an available connection that matches this provider. +

+ } + @if (host.can("connection.manage")) { + @if (!connectionOptions.length) { +

+ No matching provider connections yet. Create one with an + operator-approved secret handle, or change the provider. +

+ } +
+ +
+ @if (newConnection) { + + } + } +
+
+
+ @for (row of settingsSummary; track row.label) { +
+
{{ row.label }}
+
{{ row.value }}
+
+ } +
+

+ Saving applies these settings to Lumi in the current + environment. It does not change workflow AI profiles or send + a model request. The platform operator must enable the Lumi + gateway and authorize this connection before you can ask + Lumi. +

+
+
+ @if (configError) { + + } +
+
- @if (newConnection) { - - } - } + + } @else if (configBusy) { +

Loading Lumi settings…

} - @if (configError) { + @if (!configSchema && configError) { }
@@ -366,6 +426,38 @@ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; flex-wrap: wrap; align-items: center; } + [hidden] { + display: none !important; + } + .settings-connection { + display: grid; + gap: 12px; + min-width: 0; + } + .settings-summary { + margin: 0; + border-top: 1px solid var(--line); + } + .settings-summary div { + display: grid; + grid-template-columns: minmax(100px, 1fr) minmax(0, 2fr); + gap: 16px; + padding: 12px 0; + border-bottom: 1px solid var(--line); + } + .settings-summary dt { + color: var(--muted); + } + .settings-summary dd { + margin: 0; + overflow-wrap: anywhere; + } + @media (max-width: 480px) { + .settings-summary div { + grid-template-columns: 1fr; + gap: 4px; + } + } .lumi-conversation { display: grid; gap: 16px; @@ -456,6 +548,46 @@ export class LumiPanel implements DoCheck { review: ProposalReview | null = null; validating = false; settings = false; + settingsStep = 0; + readonly settingsHeadings = [ + "Choose Lumi's model", + "Choose a provider connection", + "Review Lumi settings", + ]; + get settingsSummary() { + const profile = object(this.config["profile"]); + const connection = this.connections.find( + (c) => c.id === this.configConnection, + ); + return [ + { + label: "Assistant", + value: this.config["enabled"] ? "Enabled" : "Disabled", + }, + { label: "Provider", value: profile["provider"] }, + { label: "Model or deployment", value: profile["model"] }, + { + label: "Connection", + value: connection + ? `${connection.name} · revision ${connection.revision}` + : "Unavailable", + }, + { + label: "Endpoint", + value: connection?.config.endpoint ?? "Unavailable", + }, + { + label: "Reasoning", + value: object(profile["reasoning"])["pattern"] ?? "none", + }, + { label: "Maximum calls", value: profile["maxCalls"] }, + { label: "Timeout", value: `${profile["timeoutSeconds"]} seconds` }, + { + label: "Maximum output tokens", + value: object(profile["options"])["max_tokens"] ?? "Provider default", + }, + ]; + } configBusy = false; configError = ""; configSchema: Schema | null = null; @@ -509,6 +641,7 @@ export class LumiPanel implements DoCheck { this.replySchema = undefined; } clear() { + if (this.busy || this.configBusy) return; this.conversation.clear(); this.message = ""; this.review = null; @@ -762,19 +895,28 @@ export class LumiPanel implements DoCheck { ]; this.configConnection = connection.id; } - get configComplete() { + get modelComplete() { return ( - this.host.can("connection.manage") && - this.selectedConnection && !!this.configSchema && this.configValid && !missingData(this.configSchema, this.config).length ); } + get configComplete() { + return ( + this.host.can("connection.manage") && + this.selectedConnection && + this.modelComplete + ); + } async openSettings() { - if (!this.host.profile || !this.host.can("lumi.manage")) return; + if (this.configBusy || !this.host.profile || !this.host.can("lumi.manage")) + return; this.keepFocus(); this.settings = true; + this.settingsStep = 0; + this.newConnection = false; + this.configSchema = null; this.keepFocus(); this.configBusy = true; this.configError = ""; @@ -849,7 +991,13 @@ export class LumiPanel implements DoCheck { } } async saveSettings() { - if (!this.configComplete || !this.host.can("lumi.manage")) return; + if ( + this.configBusy || + this.settingsStep !== 2 || + !this.configComplete || + !this.host.can("lumi.manage") + ) + return; this.configBusy = true; this.configError = ""; const generation = this.conversation.generation; diff --git a/studio/tests/ai-shared-context.test.ts b/studio/tests/ai-shared-context.test.ts new file mode 100644 index 00000000..b09d948b --- /dev/null +++ b/studio/tests/ai-shared-context.test.ts @@ -0,0 +1,93 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import { describe, expect, it } from "vitest"; +import { + sharedAiResults, + withSharedAiResults, + availableAiResults, +} from "../src/app/integrations/ai-shared-context"; +import { visibleRefs } from "../src/app/forms/core/scope"; + +const workflow = { + spec: { + inputSchema: { type: "object" }, + llmProfiles: { + small: { + outputSchema: { + type: "object", + properties: { answer: { type: "string" } }, + }, + }, + }, + steps: [ + { id: "research", kind: "llm", profile: "small" }, + { id: "write", kind: "llm", profile: "small" }, + { id: "future", kind: "llm", profile: "small" }, + ], + }, +}; +describe("explicit shared AI context within one execution", () => { + it("offers only prior AI result roots visible at this step", () => { + const refs = availableAiResults(visibleRefs(workflow, "write", "/context")); + expect(refs.map((ref) => ref.stepId)).toEqual(["research"]); + }); + it("retains arbitrary existing context and restores it after deselection", () => { + const original = { + op: { name: "coalesce", args: [{ ref: "/input/data" }, { literal: {} }] }, + }; + const refs = availableAiResults(visibleRefs(workflow, "write", "/context")); + const result = withSharedAiResults(original, ["research"], refs); + expect(result).toEqual({ + object: { + data: original, + sharedAiResults: { + object: { research: { ref: "/steps/research/output/result" } }, + }, + }, + }); + expect(sharedAiResults(result)).toEqual(["research"]); + expect(withSharedAiResults(result, [], refs)).toEqual(original); + expect(original).not.toHaveProperty("object"); + }); + it("rejects unavailable, duplicate or excessive references", () => { + const refs = availableAiResults(visibleRefs(workflow, "write", "/context")); + expect(() => + withSharedAiResults({ literal: {} }, ["future"], refs), + ).toThrow("available"); + expect(() => + withSharedAiResults({ literal: {} }, ["research", "research"], refs), + ).toThrow("once"); + expect(() => + withSharedAiResults( + { literal: {} }, + Array.from({ length: 17 }, (_, i) => String(i)), + refs, + ), + ).toThrow("16"); + }); + it("does not mistake a hand-authored data object for its managed selection", () => { + const context = { + object: { + data: { literal: {} }, + sharedAiResults: { literal: "custom value" }, + }, + }; + expect(sharedAiResults(context)).toEqual([]); + expect(withSharedAiResults(context, [], [])).toEqual(context); + }); +}); diff --git a/studio/tests/browser/ai-setup.spec.ts b/studio/tests/browser/ai-setup.spec.ts index 55c207de..adccad3f 100644 --- a/studio/tests/browser/ai-setup.spec.ts +++ b/studio/tests/browser/ai-setup.spec.ts @@ -121,6 +121,7 @@ for (const width of [1440, 600]) form.getByLabel("Provider", { exact: true }), "azure-responses", ); + await form.getByRole("button", { name: "Continue", exact: true }).click(); await form .getByLabel("Provider endpoint", { exact: true }) .fill("https://approved-models.openai.azure.com/"); @@ -128,13 +129,16 @@ for (const width of [1440, 600]) .getByLabel("API key secret handle", { exact: true }) .fill("azure-model-key"); await form - .getByRole("button", { name: "Create AI connection", exact: true }) + .getByRole("button", { name: "Review connection", exact: true }) .click(); await expect(form).toContainText("Azure requires an API version"); expect(capture.creates).toHaveLength(0); await form .getByLabel("Azure API version", { exact: true }) .fill("2025-04-01-preview"); + await form + .getByRole("button", { name: "Review connection", exact: true }) + .click(); await form .getByRole("button", { name: "Create AI connection", exact: true }) .click(); @@ -169,6 +173,9 @@ for (const width of [1440, 600]) await lumi .getByLabel("Model", { exact: true }) .fill("assistant-deployment"); + await lumi + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); await selectChoice( lumi.getByLabel("Provider connection", { exact: true }), connectionId, @@ -176,6 +183,9 @@ for (const width of [1440, 600]) await expect( lumi.getByLabel("Connection revision id", { exact: true }), ).toHaveCount(0); + await lumi + .getByRole("button", { name: "Review settings", exact: true }) + .click(); await lumi .getByRole("button", { name: "Save Lumi settings", exact: true }) .click(); @@ -216,9 +226,6 @@ for (const width of [1440, 600]) .click(); await designer.selectStep("ask-ai-1"); const form = page.locator("weave-llm-inspector"); - await form - .getByRole("button", { name: "Add AI connection slot", exact: true }) - .click(); await selectChoice( form.getByLabel("Provider", { exact: true }), "azure-responses", @@ -227,6 +234,24 @@ for (const width of [1440, 600]) .getByLabel("Model", { exact: true }) .fill("workflow-deployment"); await form.getByLabel("Max tokens", { exact: true }).fill("512"); + await form + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await form + .getByRole("button", { name: "Add AI connection slot", exact: true }) + .click(); + await expect( + form.getByLabel("AI connection slot", { exact: true }), + ).toHaveValue("ai"); + await form + .getByRole("button", { name: "Review settings", exact: true }) + .click(); + await form + .getByRole("button", { + name: "Apply workflow AI settings", + exact: true, + }) + .click(); await form .locator('[data-field="prompt"]') .getByLabel("Property value", { exact: true }) @@ -234,9 +259,6 @@ for (const width of [1440, 600]) await expect( form.getByLabel("Workflow AI profile", { exact: true }), ).toHaveValue("default"); - await expect( - form.getByLabel("AI connection slot", { exact: true }), - ).toHaveValue("ai"); const doc = parse(await sourceText(page)); expect(doc.spec.llmProfiles.default.provider).toBe("azure-responses"); expect(doc.spec.llmProfiles.default.model).toBe("workflow-deployment"); @@ -250,16 +272,29 @@ for (const width of [1440, 600]) test("Lumi manager without connection management sees the required grant instead of an editable UUID", async ({ page, }) => { - await setup(page, ["lumi.use", "lumi.manage"]); + const capture = await setup(page, ["lumi.use", "lumi.manage"]); await page.getByRole("button", { name: "Ask Lumi", exact: true }).click(); const lumi = page.getByRole("dialog", { name: "Ask Lumi", exact: true }); await lumi .getByRole("button", { name: "Lumi settings", exact: true }) .click(); await expect(lumi).toContainText("connection.manage"); + await selectChoice( + lumi.getByLabel("Provider", { exact: true }), + "openai-responses", + ); + await lumi.getByLabel("Model", { exact: true }).fill("test-model"); + await lumi + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); await expect( - lumi.getByRole("button", { name: "Save Lumi settings", exact: true }), + lumi.getByRole("button", { name: "Review settings", exact: true }), ).toBeDisabled(); + await expect( + lumi.getByLabel("Provider connection", { exact: true }), + ).toBeDisabled(); + expect(capture.configurations).toEqual([]); + expect(capture.creates).toEqual([]); await expect( lumi.getByLabel("Connection revision id", { exact: true }), ).toHaveCount(0); @@ -275,13 +310,22 @@ test("Settings exposes admin AI setup while viewers cannot configure or spend", .click(); await expect( page.getByRole("heading", { - name: "Lumi model and connection", + name: "Choose Lumi's model", exact: true, }), ).toBeVisible(); + const lumi = page.getByRole("dialog", { name: "Ask Lumi", exact: true }); + await selectChoice( + lumi.getByLabel("Provider", { exact: true }), + "openai-responses", + ); + await lumi.getByLabel("Model", { exact: true }).fill("test-model"); + await lumi + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); await expect( - page.getByRole("dialog", { name: "Ask Lumi", exact: true }), - ).toContainText("No matching provider connections"); + lumi.getByText(/No matching provider connections/), + ).toBeVisible(); }); test("viewers see setup guidance without manager controls", async ({ @@ -327,7 +371,7 @@ test("provider setup explains missing catalog permission without creating a conn }); await expect(form).toContainText("catalog.read"); await expect( - form.getByRole("button", { name: "Create AI connection", exact: true }), + form.getByRole("button", { name: "Continue", exact: true }), ).toBeDisabled(); expect(capture.creates).toEqual([]); }); @@ -365,12 +409,16 @@ test("retrying an uncertain connection create reuses the same idempotency key", form.getByLabel("Provider", { exact: true }), "openai-responses", ); + await form.getByRole("button", { name: "Continue", exact: true }).click(); await form .getByLabel("Provider endpoint", { exact: true }) .fill("https://api.openai.com/v1"); await form .getByLabel("API key secret handle", { exact: true }) .fill("model-key"); + await form + .getByRole("button", { name: "Review connection", exact: true }) + .click(); const submit = form.getByRole("button", { name: "Create AI connection", exact: true, @@ -383,3 +431,117 @@ test("retrying an uncertain connection create reuses the same idempotency key", expect(requests[0].key).toBeTruthy(); expect(requests[1]).toEqual(requests[0]); }); + +for (const width of [1440, 360]) + test.describe(`AI connection wizard at ${width}`, () => { + test.use({ viewport: { width, height: 800 } }); + test("wizard guides keyboard review without creating early and retains Back edits", async ({ + page, + }) => { + const capture = await setup(page); + await page + .getByRole("button", { name: "Connections", exact: true }) + .click(); + await page + .getByRole("button", { name: "New AI connection", exact: true }) + .click(); + const form = page.getByRole("dialog", { + name: "New AI connection", + exact: true, + }); + const progress = form.getByRole("list", { name: "AI connection setup" }); + await expect(progress.locator('[aria-current="step"]')).toHaveText( + /Provider/, + ); + await expect( + form.getByLabel("Provider endpoint", { exact: true }), + ).toHaveCount(0); + await form.getByRole("button", { name: "Continue", exact: true }).click(); + await expect(form.getByRole("alert")).toContainText("connection name"); + expect(capture.creates).toHaveLength(0); + await selectChoice( + form.getByLabel("Provider", { exact: true }), + "azure-chat", + ); + await form + .getByLabel("Connection name", { exact: true }) + .fill("approved-azure"); + await form.getByLabel("Connection name", { exact: true }).press("Enter"); + await expect( + form.getByRole("heading", { + name: "Endpoint and secret handle", + exact: true, + }), + ).toBeFocused(); + await expect(progress.locator('[aria-current="step"]')).toHaveText( + /Access/, + ); + await expect( + form.getByLabel("Provider endpoint", { exact: true }), + ).toHaveAttribute( + "placeholder", + "https://your-resource.openai.azure.com", + ); + await expect(form).toContainText("operator-approved endpoint"); + await form + .getByLabel("Provider endpoint", { exact: true }) + .fill("https://approved.openai.azure.com"); + await form + .getByLabel("Azure API version", { exact: true }) + .fill("2024-10-21"); + await form + .getByLabel("API key secret handle", { exact: true }) + .fill("approved-key"); + await form + .getByLabel("API key secret handle", { exact: true }) + .press("Enter"); + await expect( + form.getByRole("heading", { + name: "Review your connection", + exact: true, + }), + ).toBeFocused(); + await expect(progress.locator('[aria-current="step"]')).toHaveText( + /Review/, + ); + await expect(form).toContainText("Azure OpenAI Chat"); + await expect(form).toContainText("https://approved.openai.azure.com"); + await expect(form).toContainText("approved-key"); + await expect(form).toContainText("No model request or connectivity test"); + expect(capture.creates).toHaveLength(0); + await form.getByRole("button", { name: "Back", exact: true }).click(); + await expect( + form.getByLabel("API key secret handle", { exact: true }), + ).toHaveValue("approved-key"); + await form.getByRole("button", { name: "Back", exact: true }).click(); + await expect( + form.getByLabel("Connection name", { exact: true }), + ).toHaveValue("approved-azure"); + await form + .getByLabel("Connection name", { exact: true }) + .fill("reviewed-azure"); + await form.getByRole("button", { name: "Continue", exact: true }).click(); + await expect( + form.getByLabel("Azure API version", { exact: true }), + ).toHaveValue("2024-10-21"); + await form + .getByRole("button", { name: "Review connection", exact: true }) + .click(); + await expect(form).toContainText("reviewed-azure"); + expect(capture.creates).toHaveLength(0); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= innerWidth, + ), + ).toBe(true); + await page.screenshot({ + path: `../.superpowers/editor-ux/ai-wizard-review-${width}.png`, + }); + await form + .getByRole("button", { name: "Create AI connection", exact: true }) + .press("Enter"); + await expect(form).toContainText("Connection created"); + expect(capture.creates).toHaveLength(1); + expect(capture.creates[0].name).toBe("reviewed-azure"); + }); + }); diff --git a/studio/tests/browser/ai-workflow-wizard.spec.ts b/studio/tests/browser/ai-workflow-wizard.spec.ts new file mode 100644 index 00000000..38133768 --- /dev/null +++ b/studio/tests/browser/ai-workflow-wizard.spec.ts @@ -0,0 +1,235 @@ +/* +Copyright 2026 Firefly Software Foundation. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +Author: Firefly Software Foundation +SPDX-License-Identifier: Apache-2.0 +*/ +import { execFileSync } from "node:child_process"; +import { test, expect, type Page } from "@playwright/test"; +import { parse } from "yaml"; +import { offline, sourceText } from "./support"; +import { DesignerPage } from "./designer-po"; + +const profileSchema = JSON.parse( + execFileSync( + ".venv/bin/python", + [ + "-c", + "import json; from firefly_weave.contracts.llm import LLMProfile; print(json.dumps(LLMProfile.model_json_schema(by_alias=True)))", + ], + { cwd: "..", encoding: "utf8" }, + ), +); +const source = `apiVersion: weave/v1alpha1 +kind: Workflow +metadata: {name: ai-review, version: 1.0.0} +spec: + inputSchema: {type: object} + outputSchema: {} + connections: + ai: {connector: weave-agentic-provider@1.0.0, required: true} + llmProfiles: + default: + provider: azure-responses + model: original-deployment + options: {max_tokens: 512} + outputSchema: {type: object, properties: {summary: {type: string}}} + steps: + - {id: earlier, kind: llm, uses: weave-agentic-generate@1.0.0, profile: default, connection: ai, prompt: {literal: First}, context: {ref: /input}} + - {id: review, kind: llm, uses: weave-agentic-generate@1.0.0, profile: default, connection: ai, prompt: {literal: Review}, context: {literal: {note: Keep this context}}} + - {id: later, kind: llm, uses: weave-agentic-generate@1.0.0, profile: default, connection: ai, prompt: {literal: Later}, context: {ref: /input}} + output: {ref: /steps/review/output/result} +`; +async function open(page: Page) { + await offline(page); + await page.route("**/studio/contracts/llm-profile", (route) => + route.fulfill({ json: profileSchema }), + ); + await page.getByLabel("Choose a workflow file").setInputFiles({ + name: "ai-review.yaml", + mimeType: "text/yaml", + buffer: Buffer.from(source), + }); + const designer = new DesignerPage(page); + await designer.open(); + await designer.selectStep("review"); + return page.locator("weave-llm-inspector"); +} +for (const width of [1440, 600]) + test.describe(`Workflow AI setup at ${width}`, () => { + test.use({ viewport: { width, height: 900 } }); + test("profile edits remain a local draft until Apply", async ({ page }) => { + const form = await open(page); + await form + .getByText("Configure workflow AI profiles", { exact: true }) + .click(); + await form + .getByLabel("Model", { exact: true }) + .fill("unapplied-deployment"); + await form + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await form + .getByRole("button", { name: "Add AI connection slot", exact: true }) + .click(); + await expect( + form.getByLabel("AI connection slot", { exact: true }), + ).toHaveValue("ai-2"); + const document = parse(await sourceText(page)); + expect(document.spec.llmProfiles.default.model).toBe( + "original-deployment", + ); + expect(document.spec.steps[1].connection).toBe("ai"); + expect(Object.keys(document.spec.connections)).toEqual(["ai"]); + }); + test("Back preserves the draft and Review shows affected steps before applying", async ({ + page, + }) => { + const form = await open(page); + await form + .getByText("Configure workflow AI profiles", { exact: true }) + .click(); + await form + .getByLabel("Model", { exact: true }) + .fill("reviewed-deployment"); + await form.getByLabel("Max tokens", { exact: true }).fill("1024"); + await form + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await expect( + form.getByRole("heading", { + name: "Choose a workflow connection slot", + exact: true, + }), + ).toBeFocused(); + await expect( + form.getByLabel("AI connection slot", { exact: true }), + ).toHaveValue("ai"); + await form.getByRole("button", { name: "Back", exact: true }).click(); + await expect(form.getByLabel("Model", { exact: true })).toHaveValue( + "reviewed-deployment", + ); + await form + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await form + .getByRole("button", { name: "Review settings", exact: true }) + .click(); + const affected = form.getByRole("list", { + name: "Steps using this AI profile", + }); + await expect(affected.getByRole("listitem")).toHaveText([ + "earlier", + "review", + "later", + ]); + await expect(form).toContainText("Activation binds this slot"); + await form + .getByRole("button", { + name: "Apply workflow AI settings", + exact: true, + }) + .click(); + const document = parse(await sourceText(page)); + expect(document.spec.llmProfiles.default.model).toBe( + "reviewed-deployment", + ); + expect(document.spec.llmProfiles.default.options.max_tokens).toBe(1024); + expect( + document.spec.llmProfiles.default.outputSchema.properties.summary.type, + ).toBe("string"); + expect( + document.spec.steps.map((s: { connection: string }) => s.connection), + ).toEqual(["ai", "ai", "ai"]); + expect(document.spec.connections.ai).toEqual({ + connector: "weave-agentic-provider@1.0.0", + required: true, + }); + }); + test("invalid profile cannot advance and Cancel keeps the saved profile", async ({ + page, + }) => { + const form = await open(page); + await form + .getByText("Configure workflow AI profiles", { exact: true }) + .click(); + await form.getByLabel("Model", { exact: true }).fill(""); + await expect( + form.getByRole("button", { + name: "Continue to connection", + exact: true, + }), + ).toBeDisabled(); + await form + .getByRole("button", { name: "Cancel profile changes", exact: true }) + .click(); + await form + .getByText("Configure workflow AI profiles", { exact: true }) + .click(); + await expect(form.getByLabel("Model", { exact: true })).toHaveValue( + "original-deployment", + ); + await expect( + form.getByRole("button", { + name: "Continue to connection", + exact: true, + }), + ).toBeEnabled(); + expect(parse(await sourceText(page)).spec.llmProfiles.default.model).toBe( + "original-deployment", + ); + }); + test("shared context includes only explicitly selected earlier AI results", async ({ + page, + }) => { + const form = await open(page); + await expect( + form.getByRole("checkbox", { + name: "Use result from earlier", + exact: true, + }), + ).not.toBeChecked(); + await expect( + form.getByRole("checkbox", { + name: "Use result from later", + exact: true, + }), + ).toHaveCount(0); + await expect( + form.getByRole("checkbox", { + name: "Use result from review", + exact: true, + }), + ).toHaveCount(0); + await form + .getByRole("checkbox", { name: "Use result from earlier", exact: true }) + .check(); + const document = parse(await sourceText(page)); + expect(document.spec.steps[1].context).toEqual({ + object: { + data: { literal: { note: "Keep this context" } }, + sharedAiResults: { + object: { earlier: { ref: "/steps/earlier/output/result" } }, + }, + }, + }); + await new DesignerPage(page).selectStep("review"); + await form + .getByRole("checkbox", { name: "Use result from earlier", exact: true }) + .uncheck(); + expect(parse(await sourceText(page)).spec.steps[1].context).toEqual({ + literal: { note: "Keep this context" }, + }); + }); + }); diff --git a/studio/tests/browser/designer-canvas.spec.ts b/studio/tests/browser/designer-canvas.spec.ts index 1d346520..97100e1d 100644 --- a/studio/tests/browser/designer-canvas.spec.ts +++ b/studio/tests/browser/designer-canvas.spec.ts @@ -106,14 +106,11 @@ for (const viewport of [ await page.keyboard.press("ArrowDown"); await page.keyboard.press("Enter"); await expect(page.locator('[data-step="transform-1"]')).toBeVisible(); - if (viewport.width > 767) - await expect( - page.getByRole("textbox", { name: "Field name", exact: true }), - ).toBeFocused(); - else - await expect( - page.locator('[data-step="transform-1"] .node-body'), - ).toBeFocused(); + // Insertion keeps keyboard navigation on the canvas. Creating the + // inspector's initial empty row must not steal focus from the new node. + await expect( + page.locator('[data-step="transform-1"] .node-body'), + ).toBeFocused(); expect((await steps(page)).map((s) => s.id)).toEqual(["transform-1"]); }); diff --git a/studio/tests/browser/designer-po.ts b/studio/tests/browser/designer-po.ts index 3ec84ce3..e594c6b4 100644 --- a/studio/tests/browser/designer-po.ts +++ b/studio/tests/browser/designer-po.ts @@ -222,6 +222,9 @@ export class DesignerPage { * screen. */ async selectStep(stepId: string) { + // Insertion reveals its new selection after render; observe that view before + // deciding whether an earlier node needs Fit all. + await this.settled(); const body = this.node(stepId).locator(".node-body"); if (!(await this.reachable(body))) { const close = this.page.getByRole("button", { name: "Close inspector" }); diff --git a/studio/tests/browser/lumi.spec.ts b/studio/tests/browser/lumi.spec.ts index 9bffb831..80d62745 100644 --- a/studio/tests/browser/lumi.spec.ts +++ b/studio/tests/browser/lumi.spec.ts @@ -171,94 +171,163 @@ test("context defaults off, stale proposals cannot replace later edits, and relo await expect(panel).not.toContainText("Private conversation"); }); -test("Lumi settings use the canonical profile, fixed reply schema, and configuration revision", async ({ - page, -}) => { - const schema = JSON.parse( - execFileSync( - ".venv/bin/python", - [ - "-c", - "import json; from firefly_weave.contracts.lumi import LumiConfigurationRequest; print(json.dumps(LumiConfigurationRequest.model_json_schema(by_alias=True)))", - ], - { cwd: "..", encoding: "utf8" }, - ), - ); - const fixed = schema.$defs.LumiProfile.properties.outputSchema.const; - await connected(page, { - capabilities: [...allCapabilities, "lumi.use", "lumi.manage"], - }); - await page.route("**/lumi/status", (r) => - r.fulfill({ - json: { configured: true, provider: "openai-responses", model: "before" }, - }), - ); - await page.route("**/studio/contracts/lumi-configuration", (r) => - r.fulfill({ json: schema }), - ); - const original = { - enabled: true, - connection_revision_id: "11111111-1111-4111-8111-111111111111", - profile: { - provider: "openai-responses", - model: "before", - options: { max_tokens: 512 }, - reasoning: { pattern: "none", maxSteps: 6 }, - maxCalls: 8, - timeoutSeconds: 120, - outputSchema: fixed, - }, - revision: 7, - }; - await page.route("**/environments/development/connections?*", (r) => - r.fulfill({ - json: { - items: [ - { - id: original.connection_revision_id, - name: "lumi-provider", - revision: 1, - connector: "weave-agentic-provider@1.0.0", - config: { - provider: "openai-responses", - endpoint: "https://api.openai.com", - }, - }, +for (const viewport of [ + { width: 1440, height: 900 }, + { width: 390, height: 700 }, +]) + test(`Lumi settings wizard preserves drafts and saves reviewed configuration at ${viewport.width}px`, async ({ + page, + }, testInfo) => { + await page.setViewportSize(viewport); + const schema = JSON.parse( + execFileSync( + ".venv/bin/python", + [ + "-c", + "import json; from firefly_weave.contracts.lumi import LumiConfigurationRequest; print(json.dumps(LumiConfigurationRequest.model_json_schema(by_alias=True)))", ], - next_cursor: null, + { cwd: "..", encoding: "utf8" }, + ), + ); + const fixed = schema.$defs.LumiProfile.properties.outputSchema.const; + await connected(page, { + capabilities: [...allCapabilities, "lumi.use", "lumi.manage"], + }); + await page.route("**/lumi/status", (r) => + r.fulfill({ + json: { + configured: true, + provider: "openai-responses", + model: "before", + }, + }), + ); + await page.route("**/studio/contracts/lumi-configuration", (r) => + r.fulfill({ json: schema }), + ); + const original = { + enabled: true, + connection_revision_id: "11111111-1111-4111-8111-111111111111", + profile: { + provider: "openai-responses", + model: "before", + options: { max_tokens: 512 }, + reasoning: { pattern: "none", maxSteps: 6 }, + maxCalls: 8, + timeoutSeconds: 120, + outputSchema: fixed, }, - }), - ); - let saved: any; - let etag: string | undefined; - await page.route("**/lumi/configuration", (r) => { - if (r.request().method() === "PUT") { - saved = r.request().postDataJSON(); - etag = r.request().headers()["if-match"]; - return r.fulfill({ json: { ...saved, revision: 8 } }); - } - return r.fulfill({ json: original }); + revision: 7, + }; + await page.route("**/environments/development/connections?*", (r) => + r.fulfill({ + json: { + items: [ + { + id: original.connection_revision_id, + name: "lumi-provider", + revision: 1, + connector: "weave-agentic-provider@1.0.0", + config: { + provider: "openai-responses", + endpoint: "https://api.openai.com", + }, + }, + ], + next_cursor: null, + }, + }), + ); + let saved: any; + let etag: string | undefined; + let finishSave: () => void = () => {}; + const saving = new Promise((resolve) => { + finishSave = resolve; + }); + await page.route("**/lumi/configuration", async (r) => { + if (r.request().method() === "PUT") { + saved = r.request().postDataJSON(); + etag = r.request().headers()["if-match"]; + await saving; + return r.fulfill({ json: { ...saved, revision: 8 } }); + } + return r.fulfill({ json: original }); + }); + await newWorkflow(page); + await page.getByRole("button", { name: "Ask Lumi", exact: true }).click(); + const panel = page.getByRole("dialog", { name: "Ask Lumi", exact: true }); + await panel + .getByRole("button", { name: "Lumi settings", exact: true }) + .click(); + await panel.getByLabel("Model", { exact: true }).fill("configured-model"); + await panel.getByLabel("Max tokens", { exact: true }).fill("1024"); + await expect(panel.getByText("Output schema", { exact: true })).toHaveCount( + 0, + ); + await expect( + panel.getByRole("button", { name: "Save Lumi settings", exact: true }), + ).toHaveCount(0); + await panel + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await expect( + panel.getByRole("heading", { + name: "Choose a provider connection", + exact: true, + }), + ).toBeFocused(); + expect(saved).toBeUndefined(); + await panel.getByRole("button", { name: "Back", exact: true }).click(); + await expect(panel.getByLabel("Model", { exact: true })).toHaveValue( + "configured-model", + ); + await expect(panel.getByLabel("Max tokens", { exact: true })).toHaveValue( + "1024", + ); + await panel + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await panel + .getByRole("button", { name: "Review settings", exact: true }) + .click(); + await expect( + panel.getByRole("heading", { name: "Review Lumi settings", exact: true }), + ).toBeFocused(); + await expect(panel.locator(".settings-summary")).toContainText( + "configured-model", + ); + await expect(panel.locator(".settings-summary")).toContainText( + "https://api.openai.com", + ); + await expect(panel.locator(".settings-summary")).toContainText( + "lumi-provider", + ); + expect(saved).toBeUndefined(); + await panel.screenshot({ + path: testInfo.outputPath(`lumi-review-${viewport.width}.png`), + }); + expect(await panel.evaluate((el) => el.scrollWidth <= el.clientWidth)).toBe( + true, + ); + await panel + .getByRole("button", { name: "Save Lumi settings", exact: true }) + .click(); + await expect( + panel.getByRole("button", { name: "Lumi settings", exact: true }), + ).toBeDisabled(); + await expect( + panel.getByRole("button", { name: "New conversation", exact: true }), + ).toBeDisabled(); + await expect( + panel.getByRole("button", { name: "Reload settings", exact: true }), + ).toBeDisabled(); + finishSave(); + await expect(panel.getByLabel("Message to Lumi")).toBeVisible(); + expect(etag).toBe('"7"'); + expect(saved.profile.model).toBe("configured-model"); + expect(saved.profile.options.max_tokens).toBe(1024); + expect(saved.profile.outputSchema).toEqual(fixed); }); - await newWorkflow(page); - await page.getByRole("button", { name: "Ask Lumi", exact: true }).click(); - const panel = page.getByRole("dialog", { name: "Ask Lumi", exact: true }); - await panel - .getByRole("button", { name: "Lumi settings", exact: true }) - .click(); - await panel.getByLabel("Model", { exact: true }).fill("configured-model"); - await panel.getByLabel("Max tokens", { exact: true }).fill("1024"); - await expect(panel.getByText("Output schema", { exact: true })).toHaveCount( - 0, - ); - await panel - .getByRole("button", { name: "Save Lumi settings", exact: true }) - .click(); - await expect(panel.getByLabel("Message to Lumi")).toBeVisible(); - expect(etag).toBe('"7"'); - expect(saved.profile.model).toBe("configured-model"); - expect(saved.profile.options.max_tokens).toBe(1024); - expect(saved.profile.outputSchema).toEqual(fixed); -}); test("non-workflow proposals are validated before saving a reviewed file", async ({ page, diff --git a/studio/tests/browser/policy-nodes.spec.ts b/studio/tests/browser/policy-nodes.spec.ts index c08f46df..6777de03 100644 --- a/studio/tests/browser/policy-nodes.spec.ts +++ b/studio/tests/browser/policy-nodes.spec.ts @@ -174,6 +174,21 @@ for (const width of [1440, 600]) .getByLabel("Model", { exact: true }) .fill("updated-model"); await inspector.getByLabel("Max tokens", { exact: true }).fill("1024"); + await inspector + .getByRole("button", { name: "Continue to connection", exact: true }) + .click(); + await inspector + .getByRole("button", { name: "Add AI connection slot", exact: true }) + .click(); + await inspector + .getByRole("button", { name: "Review settings", exact: true }) + .click(); + await inspector + .getByRole("button", { + name: "Apply workflow AI settings", + exact: true, + }) + .click(); const doc = parse(await sourceText(page)); expect(doc.spec.steps[1].uses).toBe("weave-agentic-generate@1.1.0"); expect(doc.spec.steps[1].prompt).toEqual({ diff --git a/studio/tests/scope.test.ts b/studio/tests/scope.test.ts index abb1c7d0..05e33358 100644 --- a/studio/tests/scope.test.ts +++ b/studio/tests/scope.test.ts @@ -540,6 +540,18 @@ documents = json.load(sys.stdin) # This oracle tests reference semantics, not production timing admission. Keep # structural/work limits and a bounded regex deadline, allowing runner contention. parity_limits = replace(DEFAULT_CONTRACT_LIMITS, regex_timeout_seconds=1, max_regex_seconds=5) +# The public contracts do not change during this oracle process. Reuse their +# canonical snapshot, while giving every full validation its own mutable copy. +export_original = schemas.export_schemas +schema_exports = 0 +schema_snapshot = None +def export_snapshot(): + global schema_exports, schema_snapshot + if schema_snapshot is None: + schema_exports += 1 + schema_snapshot = json.dumps(export_original(), sort_keys=True, separators=(",", ":")) + return json.loads(schema_snapshot) +schemas.export_schemas = export_snapshot results = [] for document in documents: result = check(json.dumps(document), format="json", contract_limits=parity_limits) @@ -561,7 +573,11 @@ def with_scheduling_pause(limits): return check(json.dumps(documents[0]), format="json", contract_limits=limits) paused_default = with_scheduling_pause(DEFAULT_CONTRACT_LIMITS) paused_parity = with_scheduling_pause(parity_limits) -print(json.dumps({"mode": mode, "results": results, "budgetResult": { +owned_copy = export_snapshot() +owned_copy["workflow"].clear() +snapshot_unchanged = export_snapshot()["workflow"] == json.loads(schema_snapshot)["workflow"] +print(json.dumps({"mode": mode, "schemaExports": schema_exports, +"schemaSnapshotUnchanged": snapshot_unchanged, "results": results, "budgetResult": { "validationOk": budget_result.validation_ok, "diagnostics": [d.model_dump(by_alias=True, exclude_none=True) for d in budget_result.diagnostics], }, "pausedDefaultCodes": [d.code for d in paused_default.diagnostics], @@ -705,6 +721,8 @@ const compiled = available }), ) as { mode: "authoring" | "source"; + schemaExports: number; + schemaSnapshotUnchanged: boolean; results: CompilerResult[]; budgetResult: CompilerResult; pausedDefaultCodes: string[]; @@ -713,6 +731,10 @@ const compiled = available : null; describe.skipIf(!available)("scope parity with the Python compiler", () => { + it("exports immutable contracts once and isolates each validation's schema copy", () => { + expect(compiled!.schemaExports).toBe(1); + expect(compiled!.schemaSnapshotUnchanged).toBe(true); + }); it("isolates scope parity from a bounded runner scheduling pause", () => { expect(compiled!.pausedDefaultCodes).toContain("WV-SCHEMA-RESOURCE_LIMIT"); expect(compiled!.pausedParityDiagnostics).toEqual( diff --git a/studio/vitest.config.ts b/studio/vitest.config.ts index 2933cb79..103e6550 100644 --- a/studio/vitest.config.ts +++ b/studio/vitest.config.ts @@ -23,6 +23,8 @@ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { include: ["tests/*.test.ts"], + // Each worker can also run a Python oracle; bound both layers on CI hosts. + maxWorkers: 2, testTimeout: 240_000, hookTimeout: 240_000, }, diff --git a/tests/integration/test_llm_execution.py b/tests/integration/test_llm_execution.py index dc227c7d..ad380ab8 100644 --- a/tests/integration/test_llm_execution.py +++ b/tests/integration/test_llm_execution.py @@ -50,6 +50,7 @@ from firefly_weave.operations.history import HistoryService from firefly_weave.operations.incidents import IncidentService from firefly_weave.runtime.service import RuntimeService +from firefly_weave.runtime.signals import SignalService from firefly_weave.workers.leases import TaskService from firefly_weave.workers.service import WorkerService @@ -113,13 +114,15 @@ async def llm_task(services, access_db, provisioned, monkeypatch, request): await definitions.publish( actor, scope, "Action", json.dumps(action_definition()), "json", "action", context=AuditContext() ) + shared_context = getattr(request, "param", None) == "shared-context" + result_schema = {"type": "object", "properties": {"marker": {"type": "string"}}, "required": ["marker"]} profile = { "provider": "openai-chat", "model": "fixture", "options": {"max_tokens": 256}, "maxCalls": 2, "timeoutSeconds": 10, - "outputSchema": getattr(request, "param", {"type": "boolean"}), + "outputSchema": result_schema if shared_context else getattr(request, "param", {"type": "boolean"}), } source = { "apiVersion": "weave/v1alpha1", @@ -145,6 +148,20 @@ async def llm_task(services, access_db, provisioned, monkeypatch, request): "output": {"literal": True} if hasattr(request, "param") else {"ref": "/steps/ask/output/result"}, }, } + if shared_context: + source["spec"]["outputSchema"] = result_schema + source["spec"]["steps"].extend( + [ + {"id": "pause", "kind": "signal", "name": "continue", "timeoutSeconds": 60, "payloadSchema": {}}, + { + **source["spec"]["steps"][0], + "id": "followup", + "context": {"ref": "/steps/ask/output/result"}, + }, + {**source["spec"]["steps"][0], "id": "independent"}, + ] + ) + source["spec"]["output"] = {"ref": "/steps/followup/output/result"} published = await definitions.publish( actor, scope, "Workflow", json.dumps(source), "json", "flow", context=AuditContext() ) @@ -189,6 +206,9 @@ async def llm_task(services, access_db, provisioned, monkeypatch, request): release=release, capability=capability, profile=profile, + activation=activation, + instance=instance, + reload=lambda: services(access_db[0], registry=registry, secrets=secrets), ) @@ -314,3 +334,77 @@ async def test_model_profile_secret_output_cannot_enter_durable_history(llm_task exported = await setup.history.export(tx, setup.run.id, **setup.authority) assert exported.events[-1].type == "incident_opened" assert exported.events[-1].data["code"] == "WV-SCHEMA-SECRET_VALUE" + + +@pytest.mark.parametrize("llm_task", ["shared-context"], indirect=True) +async def test_explicit_ai_context_survives_wait_reload_and_completion_retry_without_cross_run_memory(llm_task): + setup = llm_task + actor, scope = setup.authority["actor"], setup.authority["scope"] + first_result = {"marker": "first-execution"} + second_result = {"marker": "second-execution"} + first_completion = uuid4() + async with setup.definitions.transaction(scope, None) as tx: + await setup.tasks.complete( + tx, setup.lease.proof, first_completion, {**valid_output(), "result": first_result}, **setup.authority + ) + graph = setup.reload() + runtime = graph.resolve(RuntimeService) + second = await runtime.start( + actor, + scope, + StartRunRequest(activation_id=setup.activation.id, input={"text": "Second execution"}), + "second-context-run", + context=AuditContext(), + ) + async with setup.definitions.transaction(scope, None) as tx: + second_lease = (await graph.resolve(TaskService).claim(tx, setup.instance.id, 1, **setup.authority))[0] + assert second_lease.input["context"] == {} + assert second_lease.input["prompt"] == "Second execution" + await graph.resolve(TaskService).complete( + tx, second_lease.proof, uuid4(), {**valid_output(), "result": second_result}, **setup.authority + ) + for run, expected, prompt in ( + (setup.run, first_result, "Return true"), + (second, second_result, "Second execution"), + ): + graph = setup.reload() + waiting = await graph.resolve(RuntimeService).read(actor, scope, run.id, context=AuditContext()) + assert waiting.state.status == "waiting" + assert waiting.state.active == ["pause"] + assert waiting.state.steps["ask"]["output"]["result"] == expected + async with setup.definitions.transaction(scope, None) as tx: + await graph.resolve(SignalService).deliver(tx, run.id, "resume", "continue", {}, **setup.authority) + graph = setup.reload() + tasks = graph.resolve(TaskService) + async with setup.definitions.transaction(scope, None) as tx: + followup = (await tasks.claim(tx, setup.instance.id, 1, **setup.authority))[0] + assert followup.input["context"] == expected + assert followup.input["prompt"] == prompt + completion = uuid4() + result = {**valid_output(), "result": expected} + with pytest.raises(RuntimeError, match="completion transaction rolled back"): + async with setup.definitions.transaction(scope, None) as tx: + await tasks.complete(tx, followup.proof, completion, result, **setup.authority) + raise RuntimeError("completion transaction rolled back") + graph = setup.reload() + tasks = graph.resolve(TaskService) + async with setup.definitions.transaction(scope, None) as tx: + context = await tasks.heartbeat(tx, followup.proof, **setup.authority) + assert context.input == followup.input + assert context.input["context"] == expected + for _ in range(2): + async with setup.definitions.transaction(scope, None) as tx: + acknowledgment = await tasks.complete(tx, followup.proof, completion, result, **setup.authority) + assert acknowledgment.status == "completed" + async with setup.definitions.transaction(scope, None) as tx: + independent = (await tasks.claim(tx, setup.instance.id, 1, **setup.authority))[0] + assert independent.input["context"] == {} + assert independent.input["prompt"] == prompt + await tasks.complete(tx, independent.proof, uuid4(), result, **setup.authority) + graph = setup.reload() + async with setup.definitions.transaction(scope, None) as tx: + exported = await graph.resolve(HistoryService).export(tx, run.id, **setup.authority) + assert exported.replay.status == "consistent" + assert exported.replay.final_state.status == "succeeded" + assert exported.replay.final_state.output == expected + assert len(exported.events) == 5 diff --git a/uv.lock b/uv.lock index d4b4ce4d..9e1a9bcc 100644 --- a/uv.lock +++ b/uv.lock @@ -706,7 +706,7 @@ wheels = [ [[package]] name = "firefly-weave" -version = "0.1.0a10" +version = "0.1.0a11" source = { editable = "." } dependencies = [ { name = "click" }, diff --git a/workers/agentic/pyproject.toml b/workers/agentic/pyproject.toml index 69e9a87a..b747846a 100644 --- a/workers/agentic/pyproject.toml +++ b/workers/agentic/pyproject.toml @@ -20,13 +20,13 @@ build-backend = "hatchling.build" [project] name = "weave-agentic-worker" -version = "0.1.2" +version = "0.1.3" license = "Apache-2.0" license-files = ["LICENSE", "NOTICE"] description = "Lease-bound Firefly Agentic model worker for Firefly Weave" requires-python = ">=3.13,<3.15" dependencies = [ - "firefly-weave==0.1.0a10", + "firefly-weave==0.1.0a11", "pyfly[oauth2-client] @ https://github.com/fireflyframework/fireflyframework-pyfly/releases/download/v26.09.15/pyfly-26.9.15-py3-none-any.whl#sha256=c712b314cbaaaaec9aa7ec7f256fb8a31e6faf228e3bd52715356fd689c8943b", "fireflyframework-agentic @ https://github.com/fireflyframework/fireflyframework-agentic/releases/download/v26.09.0/fireflyframework_agentic-26.9.0-py3-none-any.whl#sha256=a6ecb82abd54c9a1d4fa71af46593b57228ed3336e41ea0f0e821724d9bf0224", "httpx>=0.28.1,<1", diff --git a/workers/agentic/uv.lock b/workers/agentic/uv.lock index 69901dba..cfde3ae4 100644 --- a/workers/agentic/uv.lock +++ b/workers/agentic/uv.lock @@ -676,7 +676,7 @@ wheels = [ [[package]] name = "firefly-weave" -version = "0.1.0a10" +version = "0.1.0a11" source = { directory = "../../" } dependencies = [ { name = "click" }, @@ -3162,7 +3162,7 @@ wheels = [ [[package]] name = "weave-agentic-worker" -version = "0.1.2" +version = "0.1.3" source = { editable = "." } dependencies = [ { name = "firefly-weave" }, diff --git a/workers/files/pyproject.toml b/workers/files/pyproject.toml index a59b2e6c..28324800 100644 --- a/workers/files/pyproject.toml +++ b/workers/files/pyproject.toml @@ -20,12 +20,12 @@ build-backend = "hatchling.build" [project] name = "weave-files-worker" -version = "0.1.2" +version = "0.1.3" license = "Apache-2.0" license-files = ["LICENSE", "NOTICE"] description = "Lease-bound FTP, SFTP and cloud-drive transfers for Firefly Weave" requires-python = ">=3.12,<3.15" -dependencies = ["firefly-weave[worker]==0.1.0a10", "httpx>=0.28.1,<1", "aioftp==0.28.0", "asyncssh==2.24.0"] +dependencies = ["firefly-weave[worker]==0.1.0a11", "httpx>=0.28.1,<1", "aioftp==0.28.0", "asyncssh==2.24.0"] [project.scripts] weave-files-worker = "weave_files_worker.main:run" diff --git a/workers/files/uv.lock b/workers/files/uv.lock index 5200e17a..3bac6b3f 100644 --- a/workers/files/uv.lock +++ b/workers/files/uv.lock @@ -230,7 +230,7 @@ wheels = [ [[package]] name = "firefly-weave" -version = "0.1.0a10" +version = "0.1.0a11" source = { directory = "../../" } dependencies = [ { name = "click" }, @@ -1295,7 +1295,7 @@ wheels = [ [[package]] name = "weave-files-worker" -version = "0.1.2" +version = "0.1.3" source = { editable = "." } dependencies = [ { name = "aioftp" },