diff --git a/docs/decisions.md b/docs/decisions.md index 038842aa..5c70257a 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -10551,7 +10551,9 @@ reference is [integrations](integrations.md#the-composio-organization-of-an-envi missing from the Folder, its manifest unreadable), the rule is `unverified`: never taken for the person's choice and never guessed (Pablo's review of #372). Any other Environment, a personal one or a computer, works in the - organization the person chose. *(Amended 2026-10-13, after rc.65:)* the owning + organization the person chose *(amended 2026-10-26: a personal Environment works + in the account's default organization, see the addendum of that day)*. + *(Amended 2026-10-13, after rc.65:)* the owning Organization is the one Organization settings select (F45): `selectCatalogOrganization` with the binding's `owner.organization`, the manifest slug compared case-insensitively as GitHub compares slugs, else the directory @@ -10601,6 +10603,7 @@ reference is [integrations](integrations.md#the-composio-organization-of-an-envi Environment's sign-in dialog advises the default organization, the one Composio made at the sign-up, and the person chooses it. Once the CLI prints when an organization was created, the oldest one can be chosen by the rule. + *(Done on 2026-10-26, see the addendum of that day.)* | Alternative | Trade-off / disposition | | --- | --- | @@ -11473,6 +11476,73 @@ English: on main, after the sign-in Gmail's "Připojit" landed on Settings → T it stays on Integrace with its form, a card read before the sign-in connects after a live check, and a Composio switched off says so on its cards. +**Addendum 2026-10-26, reported by the Operator: a personal Environment works in the +account's default Composio organization.** On his personal Environment the Operator +found the apps of a work organization where his personal ones belong. Composio's browser +sign-in lands in the organization open in Composio's dashboard, and that is often an +Organization's one, since the person creates those there (point 5 of addendum (2) of +2026-10-13); the Launchpad then left a personal Environment's organization to the +person, because the CLI did not say which organization is the default (point 7). Root +decision 0162 says the personal Environment uses the account's default organization +(its addendum of 2026-10-13, point 2), and point 7 named the missing step: the CLI +printing when each organization was created. + +1. **The default organization is the oldest.** Composio marks none as the default and + lists the newest first; the one it made at the sign-up is the account's oldest. + Lazurio's build of the CLI prints each organization's `created_at` in `orgs list` + from `0.4.3-lazurio.3` on (Lazurio/composio#4), and `defaultComposioOrganization` + takes the oldest. An account read on 2026-10-26 confirms it: its default + organization was made at the sign-up and each Organization's one weeks later, and + the list showed the default one last. +2. **Brought current like an Organization's one.** A Machine binding whose owner is a + person, the personal Environment, has the rule `default`. The Integrace reading, the + sign-in dialog of Settings → Tools and every link and removal make the default + organization current where another one is, journaled as in point 2 of addendum (2). + The dialog offers no choice there, since the next reading would undo it; Integrace + and agents read the default organization as `{name, state, default: true}`, named as + Composio names it and never rewritten as an Organization's name would be. +3. **Nothing guessed.** Where an organization has no date (Composio's own CLI, or + Lazurio's before `0.4.3-lazurio.3`), where two are equally old, or where the list is + a full page of 50 (`orgs list` prints one page, the newest first, so the oldest may + be left out; Pablo's review of Lazurio/composio#4), the verdict is `chosen`: the + person's current organization and the dialog's choice, as before this addendum. + Where the default organization exists but cannot be made current, nothing of + Composio counts, as in point 4. +4. **Unchanged:** an Organization's Environment and a Team Environment (the rule of the + Organization's name), and a computer, where the person chooses. +5. **The pin moves to `0.4.3-lazurio.3`.** `composioBuild` names it, the installer at the + release's own commit `5bcde7de8dc78681e8d44262fcf134afab19e091` (the squash of + Lazurio/composio#4; its `install.sh` byte for byte the one of `0.4.3-lazurio.2`) and + the four archives' SHA-256 as the release's `checksums.txt` lists them and the + downloaded archives hash (read 2026-10-26). An Environment with `0.4.3-lazurio.2` + keeps the person's choice until the person updates Composio in Settings → Tools, + which then says that it is not the build Lazurio uses, as with every pin (note of + 2026-10-19). The release ran its own installation check on Linux and macOS; its + macOS arm64 build, run in an isolated home with a copy of one real account's + sign-in, printed each organization's `created_at` and the default one as the oldest. + +| Alternative | Trade-off / disposition | +| --- | --- | +| The default organization by a name ending in `_workspace` | No CLI change; but a guess about Composio's naming; rejected as in addendum (2) | +| The default organization as the last one `orgs list` prints | No CLI change; but Composio documents no order; rejected | +| Remember the person's choice for the Environment | Survives a new sign-in; but the first sign-in still lands in the open organization, and root decision 0162 names the default one; rejected | +| Read the dates from Composio's API in the Launchpad | No CLI release; but a second way to Composio with the person's key beside the one CLI (root decision 0162, addendum of 2026-10-08, point 4); rejected | + +Verified by tests: the dates kept as `createdAt` where they are dates and left out +otherwise (`tests/tools-login.test.ts`); the oldest organization as the default, and +`chosen` without a date for each, between two equally old, without any and on a full +page of 50; the rule +`default` on a personal Environment and `chosen` on a computer; on a personal +Environment the sign-in dialog and the Integrace reading making the default +organization current after a sign-in that landed in another one, the dialog with +nothing to choose, an organization made current behind the reading switched back and +a link going to the default organization, switched back first; a default organization +that cannot be made current with no account listed, a link refused, and the page and +`lazurio integrations list` naming it as Composio does; a CLI without dates leaving +the choice to the person as before (`tests/integrations-composio-organization.test.ts`); +Začínáme's Composio step done only where the default organization is current +(`tests/first-run.test.ts`). + ## F43 — The Environment vault: one Vaultwarden account per Environment, its Bitwarden CLI pinned, one session for its agents > **Superseded in part by [F53](#f53--every-environment-owns-its-vault-its-own-organization-in-the-networks-vaultwarden-created-by-its-technical-account) diff --git a/docs/environment-tools.md b/docs/environment-tools.md index d3fd976b..77b0120f 100644 --- a/docs/environment-tools.md +++ b/docs/environment-tools.md @@ -335,7 +335,7 @@ an agent (`gogcli`, `neon`) are refused with a pointer to `lazurio tools prompt immutable), downloaded into a private file first, then run with `COMPOSIO_INSTALL_PLUGINS=0 COMPOSIO_INSTALL_SHELL=none COMPOSIO_INSTALL_HELP=0 COMPOSIO_GITHUB_OWNER=Lazurio COMPOSIO_GITHUB_REPO=composio - COMPOSIO_INSTALL_VERSION=@composio/cli@0.4.3-lazurio.2` and the target's + COMPOSIO_INSTALL_VERSION=@composio/cli@0.4.3-lazurio.3` and the target's `COMPOSIO_INSTALL_SHA256` (of the archive the installer downloads: on macOS an x64 process that Rosetta 2 translates gets the arm64 one, as its `detect_target` chooses; bundle in `~/.composio`, link in `~/.local/bin`, no agent diff --git a/docs/integrations.md b/docs/integrations.md index b07f3510..d5baccb7 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -358,14 +358,14 @@ returned or logged. Decision F42, addendum (3) of 2026-10-13 (Matěj's decision), and its note of 2026-10-19. Composio's stable CLI cannot remove an account without a prompt, names no account but by an alias and notices a finished sign-in late, so Lazurio installs its -own build until a stable release of Composio's has all three changes (proposed -upstream in ComposioHQ/composio#4817, issue #4816, and #4809; the login's poll not -yet). +own build until a stable release of Composio's has all four changes (proposed +upstream in ComposioHQ/composio#4817, issue #4816, and #4809; the login's poll and +the organizations' dates not yet). -- **The build.** Composio's stable 0.4.3 with exactly three changes, the GitHub - release `@composio/cli@0.4.3-lazurio.2` of the fork Lazurio/composio (assets +- **The build.** Composio's stable 0.4.3 with exactly four changes, the GitHub + release `@composio/cli@0.4.3-lazurio.3` of the fork Lazurio/composio (assets `composio-.zip` and `checksums.txt`, as Composio's); `composio --version` - prints `0.4.3-lazurio.2`. `connections remove --yes` (and `-y`) removes + prints `0.4.3-lazurio.3`. `connections remove --yes` (and `-y`) removes without a prompt; `connections list` prints on every entry its `id` (`ca_…`), its `alias` wherever it has one (a toolkit's only account too; `alias: null` may stand for none) and, where Composio knows the provider's identity, `display_name` @@ -376,14 +376,18 @@ yet). minutes (`--key`: about 75 s), where Composio's own pause doubles from the sixth check on (checks at about 0, 5, 10, 15, 20, 25, 35, 54, 92 and 169 s), so the Launchpad's Composio dialog noticed an "Authorize" up to about a minute late - (Lazurio/composio#3, since 0.4.3-lazurio.2). Its `composio upgrade`, update - check, skill installation and companion self-repair default to the fork's releases. + (Lazurio/composio#3, since 0.4.3-lazurio.2). `orgs list` prints each + organization's `created_at` as Composio's API returns it, by which a personal + Environment finds the account's default organization, its oldest + (Lazurio/composio#4, since 0.4.3-lazurio.3; F42, addendum of 2026-10-26). Its + `composio upgrade`, update check, skill installation and companion self-repair + default to the fork's releases. - **The pin** is one constant, `composioBuild` (`src/tools/composio-build.ts`): the fork, the version and its tag, and the commit of the fork's installer (`installerCommit`). The tag and the commit come from the same release of the fork and change together, in one reviewed change; the constant goes, and the recipe returns to Composio's installer and latest stable release, once Composio's own - stable CLI has all three changes. + stable CLI has all four changes. - **Installation** (`lazurio tools install composio`, Settings → Tools): Composio's installer from Lazurio's fork, pinned by commit (`https://raw.githubusercontent.com/Lazurio/composio//install.sh`; @@ -391,7 +395,7 @@ yet). run with `COMPOSIO_INSTALL_PLUGINS=0`, `COMPOSIO_INSTALL_SHELL=none`, `COMPOSIO_INSTALL_HELP=0`, `COMPOSIO_GITHUB_OWNER=Lazurio`, `COMPOSIO_GITHUB_REPO=composio` and - `COMPOSIO_INSTALL_VERSION=@composio/cli@0.4.3-lazurio.2`. It downloads the release's + `COMPOSIO_INSTALL_VERSION=@composio/cli@0.4.3-lazurio.3`. It downloads the release's archive for the platform, checks it against the release's `checksums.txt`, publishes the bundle into `~/.composio` beside the files already there and links `~/.local/bin/composio`. The installer served at `https://composio.dev/install` @@ -406,7 +410,7 @@ yet). installer's `detect_target` chooses), so the installer refuses any other archive before it extracts or replaces anything: a refused installation or update changes nothing. The installation is done only when - `composio --version` then prints exactly `0.4.3-lazurio.2`. Were another version to + `composio --version` then prints exactly `0.4.3-lazurio.3`. Were another version to run after an installer that ignored the pins, it would fail at `verify` (`version-mismatch`), and what runs would stay. - **Update** (`lazurio tools update composio`; "Aktualizovat" / "Update" in Settings → @@ -460,7 +464,7 @@ step left to do): | --- | --- | | `ExampleCompany`, the Organization "Example Company" named as Composio accepts it | that Organization's Environments | | one organization for each other Organization, named as it is | that Organization's Environments | -| the default organization (`_workspace`, made at the sign-up) | the user's personal Environment | +| the default organization (made at the sign-up, so the account's oldest) | the user's personal Environment | Nobody else is invited to these organizations and no key is shared: the user signs in to Composio with their own account, and Lazurio switches the CLI to the @@ -490,10 +494,15 @@ in: a Team's account is an ordinary Composio account. the Organization's settings, which say whether Composio is allowed, never disagree on the owner. Missing or ambiguous is `unverified` (fixed on 2026-10-13 after rc.65, whose exact comparison left every Environment of an - Organization with capitals in its slug `unverified`). Any other Environment, a personal one or a computer, works in - the organization the user chose; by advice (this page) a personal - Environment's is the default one, and the sign-in dialog offers the choice where - the account has several. A name matches by its letters and digits alone + Organization with capitals in its slug `unverified`). A personal Environment + (a Machine binding whose owner is a person) works in the account's default + organization, the one Composio made at the sign-up (F42, addendum of 2026-10-26): + Composio marks none as the default and lists the newest first, so the rule takes + the oldest by the `created_at` Lazurio's build prints from `0.4.3-lazurio.3` on + (`defaultComposioOrganization`). Where the CLI does not date every organization, + two are equally old, or `orgs list` printed a full page of 50 (the newest first, + which may leave out the oldest), it is not guessed: the user's choice stands, as on a + computer, where the sign-in dialog offers it among several. A name matches by its letters and digits alone (`organizationKey`, note of 2026-10-17, evening): its case, diacritics, spaces, punctuation and underscores do not count, so every name Composio allows for the Organization matches it ("Human and Machine", @@ -512,7 +521,9 @@ in: a Team's account is an ordinary Composio account. - **The verdict** (`composioOrganizationVerdict`), over `orgs list`: `matches` where the one organization of that name is current; `elsewhere` where it exists and another is current; `missing` where the account has none; - `ambiguous` where it has more than one, and none is guessed. + `ambiguous` where it has more than one, and none is guessed. For a personal + Environment `matches` or `elsewhere` for the default organization, and + `chosen` where it cannot be told. - **Brought current** (`convergeComposioOrganization`): the reading (only where Composio is installed, signed in, on and allowed), and the sign-in dialog of Settings → Tools after a sign-in, make an `elsewhere` organization current @@ -521,8 +532,10 @@ in: a Team's account is an ordinary Composio account. link and every removal asks it live first (`inOwnOrganization`), never the cached reading: it switches back where another organization was chosen, goes to the current one where the account has none of the name or several, and - refuses with `composio-organization` where the organization cannot be told. A - user's choice is never changed. + refuses with `composio-organization` where the organization cannot be told. + On a personal Environment it brings the default organization current the same + way, so the sign-in dialog shows no choice there. A user's choice is never + changed on a computer, nor where the default organization cannot be told. - **Missing or there more than once** (Matěj's decision of 2026-10-13): Composio works in the user's current organization, its accounts count and links and removals go there; Apps → Integrace says what to do with a link to @@ -542,7 +555,9 @@ in: a Team's account is an ordinary Composio account. `organization`: none of its accounts is listed, and a link or a removal is refused (`409 composio-organization`) with nothing written. The reading's `composio.organization` is `{name, state}` on an Organization's Environment - (the name null where it is `unverified`), null elsewhere. + (the name null where it is `unverified`), `{name, state, default: true}` on a + personal Environment (the name as Composio gives it, `matches` or `elsewhere`), + null elsewhere. - **Creating one** is the user's, in Composio's dashboard: neither its CLI nor its documented API creates or renames an organization (the API lists them, the CLI lists and switches). The sign-in dialog and Apps → Integrace find it by diff --git a/docs/launchpad-development.md b/docs/launchpad-development.md index ab3e4bdd..f9e69150 100644 --- a/docs/launchpad-development.md +++ b/docs/launchpad-development.md @@ -2516,10 +2516,10 @@ card's state is read again. What it shows: **Update** (decision F42, addendum (3) of 2026-10-13). The tools reading carries the build Lazurio pins for a tool (`pinned`; today only composio, Lazurio's build of the -Composio CLI, `0.4.3-lazurio.2` since 2026-10-19). A composio whose version is not -that build, an earlier build of Lazurio's (`0.4.3-lazurio.1`) included, says so in its -Details ("Toto není sestavení, které Lazurio používá (0.4.3-lazurio.2)." / "This is not -the build Lazurio uses (0.4.3-lazurio.2).", the amber attention state) and its row offers +Composio CLI, `0.4.3-lazurio.3` since 2026-10-26). A composio whose version is not +that build, an earlier build of Lazurio's (`0.4.3-lazurio.2`) included, says so in its +Details ("Toto není sestavení, které Lazurio používá (0.4.3-lazurio.3)." / "This is not +the build Lazurio uses (0.4.3-lazurio.3).", the amber attention state) and its row offers "Aktualizovat" / "Update" (named "Update composio") where the update can work: a `launchpad` tool this Environment offers, not refused by the Organization, installed in the standard path (an installation elsewhere is never shadowed by a second copy, so @@ -2529,7 +2529,7 @@ there is no button there). The click is the user's consent: it posts installer run per tool at a time, shared with installation, `busy` otherwise; a tool without a pinned build is refused as `update-unavailable`). While it runs the status line says that the tool is being updated; the row then says the outcome ("composio -0.4.3-lazurio.2 je nainstalovaný; přihlášení zůstává." or that the update did not +0.4.3-lazurio.3 je nainstalovaný; přihlášení zůstává." or that the update did not finish, with why under "What happened" in Details) and the tools are read again. **gh and its SSH key** (F19 addendum 2026-09-28). The gh dialog has a third step, diff --git a/scripts/preview-launchpad.ts b/scripts/preview-launchpad.ts index 861bea26..cecad1cf 100644 --- a/scripts/preview-launchpad.ts +++ b/scripts/preview-launchpad.ts @@ -34,7 +34,10 @@ // organization of the example Organization's name ("Example Company", in // Composio "ExampleCompany"), which the first reading makes current; with // `composio-org-missing` anywhere after the mode it lacks it, and Apps → -// Integrace says to create it (decision F42, addendum of 2026-10-13). With +// Integrace says to create it (decision F42, addendum of 2026-10-13). Its +// organizations are dated as Lazurio's build prints them, the account's +// default one (`example_user_workspace`) the oldest, which a personal +// preview's first reading makes current (addendum of 2026-10-26). With // `composio-signed-out` anywhere after the mode its Composio is signed out: // "Připojit" starts the CLI's own two-step sign-in, whose link opens // nothing real, and the person's approval in Composio is the sign-in's key @@ -551,10 +554,30 @@ await writeFile( organization: "Příklad s.r.o.", // The person's own organization of the example Organization's name, // which the hosted preview's first reading makes current (decision F42, - // addendum of 2026-10-13); without it, the page says to create it. + // addendum of 2026-10-13); without it, the page says to create it. The + // account's default one, made at the sign-up and so the oldest, is what + // a personal preview's first reading makes current (addendum of + // 2026-10-26). organizations: [ - { id: "org_1", name: "Příklad s.r.o." }, - ...(composioOrgMissing ? [] : [{ id: "org_2", name: "ExampleCompany" }]), + { + id: "org_0", + name: "example_user_workspace", + created_at: "2026-09-01T08:00:00.000Z", + }, + { + id: "org_1", + name: "Příklad s.r.o.", + created_at: "2026-09-20T08:00:00.000Z", + }, + ...(composioOrgMissing + ? [] + : [ + { + id: "org_2", + name: "ExampleCompany", + created_at: "2026-10-01T08:00:00.000Z", + }, + ]), ], accounts: [ { diff --git a/src/integrations/cli.ts b/src/integrations/cli.ts index 3d30fcac..7ef121bc 100644 --- a/src/integrations/cli.ts +++ b/src/integrations/cli.ts @@ -100,12 +100,16 @@ export function integrationsText(overview: IntegrationsOverview): string { ); // Composio not in the organization of this Environment's Organization's // name (decision F42, addendum (2) of 2026-10-13): the person sees to it, - // under the name Composio accepts (note of 2026-10-17). + // under the name Composio accepts (note of 2026-10-17). The account's + // default organization (addendum of 2026-10-26) by the name Composio gives + // it. const target = overview.composio.organization; const composio = target?.name == null ? null - : (composioOrganizationName(target.name) ?? target.name); + : target.default === true + ? target.name + : (composioOrganizationName(target.name) ?? target.name); if (target !== null && target.state !== "matches") lines.push( target.state === "unverified" || target.name === null diff --git a/src/integrations/composio-organization.ts b/src/integrations/composio-organization.ts index b67281e2..bdb78467 100644 --- a/src/integrations/composio-organization.ts +++ b/src/integrations/composio-organization.ts @@ -21,33 +21,41 @@ import { type ComposioEnvironment, runComposio } from "./composio-source"; // it. An Organization's Environment works in the organization of its // Organization's name, matched by its letters and digits alone // (`organizationKey`, note of 2026-10-17), which the person creates under the -// name Composio accepts (`composioOrganizationName`); a personal Environment -// and a computer work in the one the person chose (by advice the account's -// default, the one Composio made when they signed up). The CLI's `orgs list` -// prints `[{id, name, is_selected_global_org}]` and `orgs switch --org-id -// ` makes one current; Composio has no way to create an organization -// outside its dashboard (its API lists them only), so a missing one is -// created there by the person. Server only: the page draws what the reading -// says. +// name Composio accepts (`composioOrganizationName`). A person's own +// Environment works in the account's default organization, the one Composio +// made when they signed up and so the oldest (root decision 0162, addendum of +// 2026-10-13, point 2; decision F42, addendum of 2026-10-26); a computer in +// the one the person chose. The CLI's `orgs list` prints `[{id, name, +// is_selected_global_org}]`, Lazurio's build with each one's `created_at`, +// and `orgs switch --org-id ` makes one current; Composio has no way to +// create an organization outside its dashboard (its API lists them only), so +// a missing one is created there by the person. Server only: the page draws +// what the reading says. /** What decides an Environment's Composio organization: the name of its - * Organization, or the person's own choice where it has none. An - * Organization's Environment whose Organization's name cannot be read is - * `unverified`: nothing is guessed, and Composio does not write there - * (Pablo's review of #372). */ + * Organization; on a person's own Environment the account's default + * organization; on a computer the person's own choice. An Organization's + * Environment whose Organization's name cannot be read is `unverified`: + * nothing is guessed, and Composio does not write there (Pablo's review of + * #372). */ export type ComposioOrganizationRule = | Readonly<{ kind: "named"; name: string }> + | Readonly<{ kind: "default" }> | Readonly<{ kind: "chosen" }> | Readonly<{ kind: "unverified" }>; type NamedRule = Extract; -/** A rule that decides: by the name, or by the person. */ +type DefaultRule = Extract; +/** A rule the Launchpad brings current: by the name, or the default. */ +type ConvergingRule = NamedRule | DefaultRule; +/** A rule that decides: by the name, the default, or the person. */ type DecidingRule = Exclude; /** An Environment's Composio organization as its account's list reads. */ export type ComposioOrganizationVerdict = - /** The person chooses (a personal Environment, a computer). */ + /** The person chooses: on a computer, or on a person's own Environment + * where the account's default organization cannot be told. */ | Readonly<{ kind: "chosen"; current: string | null }> - /** The organization of the Organization's name is the current one. */ + /** The organization the rule names is the current one. */ | Readonly<{ kind: "matches"; organization: ComposioOrganization }> /** It exists, and another one is current. */ | Readonly<{ @@ -64,13 +72,60 @@ export type NamedVerdict = Exclude< ComposioOrganizationVerdict, { kind: "chosen" } >; +/** The verdict of the default rule: the account's default organization + * current or not; `chosen` where it cannot be told. */ +export type DefaultVerdict = Extract< + ComposioOrganizationVerdict, + { kind: "chosen" | "matches" | "elsewhere" } +>; + +/** How many organizations `orgs list` prints at most: one page of + * Composio's list (its API allows 50), the newest first; the CLI does not + * page further. */ +export const composioOrganizationsPage = 50; + +/** The account's default organization: the one Composio made when the + * account signed up, which is its oldest (`created_at`). Composio's list + * marks none as the default and lists the newest first, so only the dates + * tell (an account read on 2026-10-26: its default organization made at the + * sign-up, each Organization's one weeks later). Null where an organization + * has no date (a CLI build that does not print it), where two are equally + * old, where there is none, or where the list is a full page, which may + * leave out the oldest ones (Pablo's review of Lazurio/composio#4): which + * one it is, is not guessed. */ +export function defaultComposioOrganization( + organizations: readonly ComposioOrganization[], +): ComposioOrganization | null { + if (organizations.length >= composioOrganizationsPage) return null; + let oldest: ComposioOrganization | null = null; + let oldestAt = Number.POSITIVE_INFINITY; + let tie = false; + for (const organization of organizations) { + const at = + organization.createdAt === undefined + ? Number.NaN + : Date.parse(organization.createdAt); + if (Number.isNaN(at)) return null; + if (at < oldestAt) { + oldest = organization; + oldestAt = at; + tie = false; + } else if (at === oldestAt) tie = true; + } + return tie ? null : oldest; +} /** The verdict of the rule over the account's organizations: those whose - * name has a key of the Organization's (`organizationKeys`). */ + * name has a key of the Organization's (`organizationKeys`), or the + * account's default one (`defaultComposioOrganization`). */ export function composioOrganizationVerdict( rule: NamedRule, organizations: readonly ComposioOrganization[], ): NamedVerdict; +export function composioOrganizationVerdict( + rule: DefaultRule, + organizations: readonly ComposioOrganization[], +): DefaultVerdict; export function composioOrganizationVerdict( rule: DecidingRule, organizations: readonly ComposioOrganization[], @@ -82,6 +137,13 @@ export function composioOrganizationVerdict( const current = organizations.find((organization) => organization.current)?.name ?? null; if (rule.kind === "chosen") return { kind: "chosen", current }; + if (rule.kind === "default") { + const organization = defaultComposioOrganization(organizations); + if (organization === null) return { kind: "chosen", current }; + return organization.current + ? { kind: "matches", organization } + : { kind: "elsewhere", organization, current }; + } const keys = organizationKeys(rule.name); const named = organizations.filter((organization) => keys.includes(organizationKey(organization.name)), @@ -98,7 +160,9 @@ export function composioOrganizationVerdict( * (its Machine binding's Organization, named as the Organization's manifest * names it); `unverified` where an Organization owns it but its name cannot * be read, or has no letter or digit Composio could name an organization - * with; the person's choice where no Organization owns it. + * with; the account's default organization on a person's own Environment + * (a binding whose owner is a person, the personal Environment); the + * person's choice on a computer, which has no binding. * * The owning Organization is the one Organization settings select (F45): * `selectOwnerOrganization` with the handover's owner, a GitHub login, @@ -111,7 +175,8 @@ export async function composioOrganizationRule( folder: string, machine: MachineBinding | null, ): Promise { - if (machine?.owner.kind !== "organization") return { kind: "chosen" }; + if (machine === null) return { kind: "chosen" }; + if (machine.owner.kind !== "organization") return { kind: "default" }; try { const selection = selectOwnerOrganization( await readFolderCatalog(folder), @@ -170,16 +235,44 @@ export async function switchComposioOrganization( * where it exists and another one is current, it is made current (`composio * orgs switch`, journaled). The verdict as it then stands, with the * account's organizations; null where they cannot be read. Only a rule that - * names the organization brings anything: a person's choice is never - * changed. */ + * names the organization, by its name or as the account's default, brings + * anything: a person's choice is never changed, nor is anything where the + * default cannot be told. */ export async function convergeComposioOrganization( input: ComposioEnvironment, command: string, rule: NamedRule, - journal: (entry: Readonly>) => void = () => {}, + journal?: (entry: Readonly>) => void, ): Promise | null>; +export async function convergeComposioOrganization( + input: ComposioEnvironment, + command: string, + rule: DefaultRule, + journal?: (entry: Readonly>) => void, +): Promise | null>; +export async function convergeComposioOrganization( + input: ComposioEnvironment, + command: string, + rule: ConvergingRule, + journal?: (entry: Readonly>) => void, +): Promise | null>; +export async function convergeComposioOrganization( + input: ComposioEnvironment, + command: string, + rule: ConvergingRule, + journal: (entry: Readonly>) => void = () => {}, +): Promise | null> { const organizations = await listComposioOrganizations(input, command); if (organizations === null) return null; diff --git a/src/integrations/connect.ts b/src/integrations/connect.ts index 827fb32d..fa0e7a10 100644 --- a/src/integrations/connect.ts +++ b/src/integrations/connect.ts @@ -979,7 +979,10 @@ export function createConnectSessions(host: () => ConnectHost) { * is; in the person's current one where the account has none of that name * or more than one (the Operator's decision of 2026-10-13: a notice, * nothing breaks); never elsewhere while it exists, and never where the - * Organization's name cannot be read. Where the person chooses, always. */ + * Organization's name cannot be read. On a person's own Environment in the + * account's default organization, made current likewise, and in the + * person's current one where the CLI does not date the organizations + * (addendum of 2026-10-26). Where the person chooses, always. */ async function inOwnOrganization( composio: NonNullable>>, ): Promise { diff --git a/src/integrations/model.ts b/src/integrations/model.ts index 0371fe2c..8d5a5ece 100644 --- a/src/integrations/model.ts +++ b/src/integrations/model.ts @@ -195,12 +195,16 @@ export type IntegrationsOverview = Readonly<{ * of 2026-10-13): current; missing or there more than once, where * Composio works in the person's current organization with a notice; * there but not made current, or `unverified` where the Organization's - * name (null) cannot be read, where nothing of Composio counts. Null - * where the person chooses the organization (a personal Environment, a - * computer) or it was not read. */ + * name (null) cannot be read, where nothing of Composio counts. On a + * person's own Environment the account's default organization + * (`default`, addendum of 2026-10-26), named as Composio names it: + * current, or not made current. Null where the person chooses the + * organization (a computer, or a personal Environment whose + * organizations the CLI does not date) or it was not read. */ organization: Readonly<{ name: string | null; state: "matches" | "missing" | "ambiguous" | "elsewhere" | "unverified"; + default?: true; }> | null; }>; sources: Readonly<{ diff --git a/src/integrations/read.ts b/src/integrations/read.ts index 608ea0a0..b1739158 100644 --- a/src/integrations/read.ts +++ b/src/integrations/read.ts @@ -132,7 +132,8 @@ export function connectedTools(tools: readonly ToolOverview[]): AppTool[] { /** Composio as this Environment reads it: its accounts, whether its CLI * removes one without a prompt, the organization it works in, and the * organization of this Environment's Organization's name where it has one - * (decision F42, addendum of 2026-10-13), made current first. */ + * (decision F42, addendum of 2026-10-13), or the account's default one on a + * person's own Environment (addendum of 2026-10-26), made current first. */ async function readComposioSource( host: IntegrationsHost, tool: ToolOverview | undefined, @@ -171,14 +172,43 @@ async function readComposioSource( (await composioOrganizationRule(host.folder, machine)); // Nothing of Composio counts where its organization cannot be told: an // Organization's name that cannot be read (Pablo's review of #372), or an - // organization of that name that could not be made current. - const closed = (state: "unverified" | "elsewhere", name: string | null) => ({ + // organization of that name, or the account's default one, that could not + // be made current. + const closed = ( + state: "unverified" | "elsewhere", + name: string | null, + own = false, + ) => ({ composio: { state: "organization" } as const, disconnect: false, organization, - target: Object.freeze({ name, state }), + target: Object.freeze({ name, state, ...(own ? { default: true } : {}) }), }); if (rule.kind === "unverified") return closed("unverified", null); + // A person's own Environment: the account's default organization, made + // current where another one is (the sign-in lands in the one open in + // Composio's dashboard, often an Organization's). Where the CLI does not + // date the organizations, the person's current one, as before. + if (rule.kind === "default") { + const converged = await convergeComposioOrganization( + host.tools, + command, + rule, + host.journal, + ); + if (converged === null) return only({ state: "unreadable" }); + const { verdict } = converged; + if (verdict.kind === "elsewhere") + return closed("elsewhere", verdict.organization.name, true); + if (verdict.kind === "matches") { + target = Object.freeze({ + name: verdict.organization.name, + state: "matches", + default: true, + }); + organization = verdict.organization.name; + } + } if (rule.kind === "named") { const converged = await convergeComposioOrganization( host.tools, diff --git a/src/launchpad/first-run.ts b/src/launchpad/first-run.ts index f22652b2..f702e85e 100644 --- a/src/launchpad/first-run.ts +++ b/src/launchpad/first-run.ts @@ -104,7 +104,9 @@ export function githubAccount(overview: ToolsReading): string | null { * `unfinished` where it is signed in, but the organizations' reading says it * does not work in the organization of this Environment's Organization's * name (missing, there twice, not made current, or the name unreadable; - * decision F42, note of 2026-10-19). */ + * decision F42, note of 2026-10-19), or on a person's own Environment that + * the account's default organization could not be made current (addendum of + * 2026-10-26). */ export type ComposioFact = "missing" | "unfinished" | "connected" | "none"; /** `organization` is the rule of the last answer of @@ -119,7 +121,8 @@ export function composioFact( if (composio.signIn?.state !== "signed-in") return "missing"; return organization === null || organization.kind === "chosen" || - (organization.kind === "named" && organization.state === "matches") + ((organization.kind === "named" || organization.kind === "default") && + organization.state === "matches") ? "connected" : "unfinished"; } diff --git a/src/launchpad/integrations-view.ts b/src/launchpad/integrations-view.ts index cb49da83..66ddb316 100644 --- a/src/launchpad/integrations-view.ts +++ b/src/launchpad/integrations-view.ts @@ -57,11 +57,19 @@ const organizationStates = new Set([ ]); /** The Composio organization of the reading: null, or its name and state; - * the name null only where the Organization's name cannot be read. */ + * the name null only where the Organization's name cannot be read. The + * account's default one (`default`) is only current or not made current. */ function isTarget(value: unknown): boolean { if (value === null) return true; if (typeof value !== "object") return false; const target = value as Record; + if (target.default !== undefined) + return ( + target.default === true && + typeof target.name === "string" && + target.name !== "" && + (target.state === "matches" || target.state === "elsewhere") + ); if (target.state === "unverified") return target.name === null; return ( typeof target.name === "string" && @@ -787,7 +795,10 @@ export function composioOrganizationChanged( const shown = overview?.composio.organization ?? null; if (shown === null || organizationChoices(answer) === null) return false; const rule = organizationRule(answer); - return rule.kind !== "named" || rule.state !== shown.state; + return ( + (rule.kind !== "named" && rule.kind !== "default") || + rule.state !== shown.state + ); } /** One line per source that could not be read, so a missing source is @@ -821,7 +832,8 @@ export function sourceLines( * the page waits for it by itself (`composioOrganizationWaits`), so the line * ends with the sign-in dialog's own waiting line. In a Team Environment * (`team`) the account is the Team's Composio account, not the person's - * (notes of 2026-10-19). */ + * (notes of 2026-10-19). On a person's own Environment the account's default + * organization is named as Composio names it (addendum of 2026-10-26). */ export function composioOrganizationLine( overview: IntegrationsOverview, copy: Copy, @@ -830,7 +842,11 @@ export function composioOrganizationLine( const target = overview.composio.organization; if (target === null || target.state === "matches") return null; const composio = - target.name === null ? null : composioOrganizationName(target.name); + target.name === null + ? null + : target.default === true + ? target.name + : composioOrganizationName(target.name); if ( target.state === "unverified" || target.name === null || diff --git a/src/launchpad/server.ts b/src/launchpad/server.ts index bb2a7158..ce27e689 100644 --- a/src/launchpad/server.ts +++ b/src/launchpad/server.ts @@ -2454,8 +2454,10 @@ export async function startLaunchpad( server.timeout(request, 60); // An Organization's Environment works in the Composio // organization of its Organization's name, made current here - // after the sign-in (decision F42, addendum of 2026-10-13); the - // person chooses elsewhere. + // after the sign-in (decision F42, addendum of 2026-10-13); a + // person's own Environment in the account's default one, made + // current likewise (addendum of 2026-10-26); the person chooses + // on a computer. const { preferences } = await withFolderReadLock(state, () => readFolderState(state), ); @@ -2488,6 +2490,48 @@ export async function startLaunchpad( toolsEnvironment.path, toolsEnvironment.platform, ); + const journal = (entry: Readonly>) => { + console.log(JSON.stringify(entry)); + // Composio's sign-in now names another organization. + if (entry.outcome === "switched") toolsReading.forget(); + }; + const unreadable = { + kind: "composio-organizations-failed", + reason: "unreadable", + } as const; + // A person's own Environment: the account's default + // organization, which the sign-in may not have landed in. + if (rule.kind === "default") { + const converged = + command === undefined + ? null + : await convergeComposioOrganization( + toolsEnvironment, + command, + rule, + journal, + ); + if (converged === null) return response(unreadable); + const { verdict, organizations } = converged; + // The CLI does not date them: the person's choice, as before. + if (verdict.kind === "chosen") + return response({ + kind: "composio-organizations", + organizations, + organization: null, + personal: true, + }); + if (verdict.kind === "matches") integrations.forget(); + return response({ + kind: "composio-organizations", + organizations, + organization: { + name: verdict.organization.name, + state: verdict.kind, + default: true, + }, + }); + } const converged = command === undefined ? null @@ -2495,17 +2539,9 @@ export async function startLaunchpad( toolsEnvironment, command, rule, - (entry) => { - console.log(JSON.stringify(entry)); - // Composio's sign-in now names another organization. - if (entry.outcome === "switched") toolsReading.forget(); - }, + journal, ); - if (converged === null) - return response({ - kind: "composio-organizations-failed", - reason: "unreadable", - }); + if (converged === null) return response(unreadable); // Found or made current: the Integrace reading the Launchpad // keeps for a minute may still say it is missing, so the next // one reads again (the end-to-end test of 2026-10-19). diff --git a/src/launchpad/tools-view.ts b/src/launchpad/tools-view.ts index fc704079..fb3d606e 100644 --- a/src/launchpad/tools-view.ts +++ b/src/launchpad/tools-view.ts @@ -1832,14 +1832,17 @@ export type OrganizationChoice = Readonly<{ /** The Composio organization this Environment's Organization's name gives * it, and how the person's account stands to it (decision F42, addendum of - * 2026-10-13); null where the person chooses it. `personal`: a personal - * Environment, where the advice is the account's default organization. */ + * 2026-10-13); on a person's own Environment the account's default one, by + * the name Composio gives it (addendum of 2026-10-26); the person's choice + * on a computer, and on a personal Environment (`personal`) whose + * organizations the CLI does not date. */ export type OrganizationRuleView = | Readonly<{ kind: "named"; name: string; state: "matches" | "missing" | "ambiguous" | "elsewhere"; }> + | Readonly<{ kind: "default"; name: string; state: "matches" | "elsewhere" }> /** An Organization owns the Environment, but its name cannot be read. */ | Readonly<{ kind: "unverified" }> | Readonly<{ kind: "chosen"; personal: boolean }>; @@ -1851,7 +1854,15 @@ export function organizationRule(input: unknown): OrganizationRuleView { : {}; const organization = value.organization; if (organization && typeof organization === "object") { - const { name, state } = organization as Record; + const { + name, + state, + default: own, + } = organization as Record; + if (own === true) + return text(name) && (state === "matches" || state === "elsewhere") + ? { kind: "default", name: name as string, state } + : { kind: "chosen", personal: true }; if (state === "unverified" && name === null) return { kind: "unverified" }; if ( text(name) && @@ -1906,8 +1917,10 @@ export function organizationChoices( * organizations cannot be read, or no organization is current, as when * the list is empty); the dialog keeps reading. * - `connected`: the account and the organization current after the - * reading made the Organization's one current; `choices` where the person - * chooses among several (a personal Environment, a computer). */ + * reading made the Organization's one, or on a person's own Environment + * the account's default one, current; `choices` where the person chooses + * among several (a computer, or a personal Environment whose organizations + * the CLI does not date). */ export type ComposioDialogView = | Readonly<{ kind: "create"; name: string }> | Readonly<{ kind: "rename"; name: string }> @@ -1948,11 +1961,19 @@ export function composioDialogView( message: fill(copy.toolsComposioOrgElsewhere, { name }), }; } + // The account's default organization, by the name Composio gives it. + if (rule.kind === "default" && rule.state === "elsewhere") + return { + kind: "problem", + message: fill(copy.toolsComposioOrgElsewhere, { name: rule.name }), + }; // Connected only with an organization current: an empty list or one with // none marked current is not connected, and the dialog reads again. if (current === null) return { kind: "problem", message: copy.toolsComposioOrgUnavailable }; - if (rule.kind === "named") + // The rule decided the organization: nothing to choose, as the next + // reading would make it current again. + if (rule.kind === "named" || rule.kind === "default") return { kind: "connected", account, organization: current, choices: null }; return { kind: "connected", diff --git a/src/tools/catalog.ts b/src/tools/catalog.ts index 1ddb1a4c..3910a7e9 100644 --- a/src/tools/catalog.ts +++ b/src/tools/catalog.ts @@ -333,8 +333,8 @@ export const toolCatalog: readonly ToolEntry[] = Object.freeze([ // another build is replaced, only by that build and with the // Operator's consent: its "Never" says so instead of the general one. installation: { - cs: `Cílový stav: \`composio\` ve standardní cestě \`~/.local/bin/composio\` je sestavení, které připíná Lazurio (${composioBuild.version}, vydání \`${composioBuild.tag}\` forku https://github.com/${composioBuild.owner}/${composioBuild.repository}: oficiální Composio CLI se třemi změnami, které potřebují Apps → Integrace a přihlášení ke Composiu). Nainstaluje ho \`lazurio tools install composio\`: instalátor Composia z forku Lazuria, připnutý na commit, s vydáním forku, bez pluginů agentů a beze změn shellu; svůj domov má v \`~/.composio\` a do \`~/.local/bin\` vede jen link. Fungující \`composio\` jiného sestavení vymění za připnuté jen \`lazurio tools update composio\` (v Nastavení → Nástroje → Aktualizovat), a to se souhlasem uživatele; přihlášení zůstane. Uživatel se přihlásí příkazem \`composio login\` a odkaz, který příkaz vrátí, otevře ve svém prohlížeči; jednotlivé aplikace pak připojuje v Apps → Integrace. Přihlášení platí pro celé toto Prostředí. Důkaz: \`composio --version\` vypíše ${composioBuild.version} a \`composio whoami\` skončí kódem 0 a jmenuje zamýšlený účet. Nikdy: tajemství (token, heslo, klíč, kód) v chatu, Gitu ani logu; druhá instalace téhož nástroje; downgrade nebo přeinstalace fungujícího \`composio\` jinak než na připnuté sestavení a se souhlasem uživatele; jiné sestavení \`composio\` než připnuté.`, - en: `Target state: \`composio\` in the standard path \`~/.local/bin/composio\` is the build Lazurio pins (${composioBuild.version}, the release \`${composioBuild.tag}\` of the fork https://github.com/${composioBuild.owner}/${composioBuild.repository}: Composio's official CLI with three changes Apps → Integrations and the sign-in to Composio need). \`lazurio tools install composio\` installs it: Composio's installer from Lazurio's fork, pinned to a commit, with the fork's release, without agent plugins and without shell changes; it keeps its home in \`~/.composio\`, with only a link in \`~/.local/bin\`. A working \`composio\` of another build is replaced by the pinned one only by \`lazurio tools update composio\` (in the Launchpad Settings → Tools → Update), with the user's consent; the sign-in stays. The user signs in with \`composio login\` and opens the link the command returns in their browser; they then connect single applications in the Launchpad under Apps → Integrations. The sign-in holds for this whole Environment. Proof: \`composio --version\` prints ${composioBuild.version} and \`composio whoami\` exits 0 and names the intended account. Never: a secret (token, password, key, code) in chat, Git or a log; a second installation of the same tool; a downgrade or reinstall of a working \`composio\` other than to the pinned build and with the user's consent; a build of \`composio\` other than the pinned one.`, + cs: `Cílový stav: \`composio\` ve standardní cestě \`~/.local/bin/composio\` je sestavení, které připíná Lazurio (${composioBuild.version}, vydání \`${composioBuild.tag}\` forku https://github.com/${composioBuild.owner}/${composioBuild.repository}: oficiální Composio CLI se čtyřmi změnami, které potřebují Apps → Integrace, přihlášení ke Composiu a výchozí organizace Composia v Osobním Prostředí). Nainstaluje ho \`lazurio tools install composio\`: instalátor Composia z forku Lazuria, připnutý na commit, s vydáním forku, bez pluginů agentů a beze změn shellu; svůj domov má v \`~/.composio\` a do \`~/.local/bin\` vede jen link. Fungující \`composio\` jiného sestavení vymění za připnuté jen \`lazurio tools update composio\` (v Nastavení → Nástroje → Aktualizovat), a to se souhlasem uživatele; přihlášení zůstane. Uživatel se přihlásí příkazem \`composio login\` a odkaz, který příkaz vrátí, otevře ve svém prohlížeči; jednotlivé aplikace pak připojuje v Apps → Integrace. Přihlášení platí pro celé toto Prostředí. Důkaz: \`composio --version\` vypíše ${composioBuild.version} a \`composio whoami\` skončí kódem 0 a jmenuje zamýšlený účet. Nikdy: tajemství (token, heslo, klíč, kód) v chatu, Gitu ani logu; druhá instalace téhož nástroje; downgrade nebo přeinstalace fungujícího \`composio\` jinak než na připnuté sestavení a se souhlasem uživatele; jiné sestavení \`composio\` než připnuté.`, + en: `Target state: \`composio\` in the standard path \`~/.local/bin/composio\` is the build Lazurio pins (${composioBuild.version}, the release \`${composioBuild.tag}\` of the fork https://github.com/${composioBuild.owner}/${composioBuild.repository}: Composio's official CLI with four changes Apps → Integrations, the sign-in to Composio and the default Composio organization of a personal Environment need). \`lazurio tools install composio\` installs it: Composio's installer from Lazurio's fork, pinned to a commit, with the fork's release, without agent plugins and without shell changes; it keeps its home in \`~/.composio\`, with only a link in \`~/.local/bin\`. A working \`composio\` of another build is replaced by the pinned one only by \`lazurio tools update composio\` (in the Launchpad Settings → Tools → Update), with the user's consent; the sign-in stays. The user signs in with \`composio login\` and opens the link the command returns in their browser; they then connect single applications in the Launchpad under Apps → Integrations. The sign-in holds for this whole Environment. Proof: \`composio --version\` prints ${composioBuild.version} and \`composio whoami\` exits 0 and names the intended account. Never: a secret (token, password, key, code) in chat, Git or a log; a second installation of the same tool; a downgrade or reinstall of a working \`composio\` other than to the pinned build and with the user's consent; a build of \`composio\` other than the pinned one.`, }, purpose: { cs: "Composio CLI pro externí aplikace (pošta, kalendář, chat a další) napojené pro celé toto Prostředí.", diff --git a/src/tools/composio-build.ts b/src/tools/composio-build.ts index dd42c976..75f7a37f 100644 --- a/src/tools/composio-build.ts +++ b/src/tools/composio-build.ts @@ -3,15 +3,18 @@ // (0.4.3) cannot remove an account without a prompt, and its `connections // list` prints no ids and no provider identity, so Apps → Integrace can // neither offer "Odpojit" nor name an account by more than an alias. The -// build is upstream stable 0.4.3 with exactly three changes, published by the +// build is upstream stable 0.4.3 with exactly four changes, published by the // fork Lazurio/composio: `connections remove --yes` (and `-y`); // `connections list` printing every account's `id`, its `alias` and, where -// Composio knows it, its `display_name`; and, since 0.4.3-lazurio.2 +// Composio knows it, its `display_name`; since 0.4.3-lazurio.2 // (Lazurio/composio#3), `composio login --poll` and the browser wait of // `composio login` checking a pending login every 5 s for up to 10 minutes // (`--key` about 75 s), where upstream's pause doubles from the sixth check // on, so the Launchpad's Composio dialog noticed an "Authorize" up to about a -// minute late (read 2026-10-19). +// minute late (read 2026-10-19); and, since 0.4.3-lazurio.3 +// (Lazurio/composio#4), `orgs list` printing each organization's +// `created_at`, by which a personal Environment finds the account's default +// organization, its oldest (decision F42, addendum of 2026-10-26). // // Why a pin: the curated installation (decision F19) and `lazurio tools // update composio` install exactly this release, and the installed `composio @@ -32,17 +35,19 @@ // and change together, here: a new build is a reviewed change of both. // // When it goes away: once a stable release of Composio's own CLI has all -// three changes (proposed upstream: `--yes` in ComposioHQ/composio#4817, -// issue #4816; the list's ids and names in #4809; the login's poll not yet), +// four changes (proposed upstream: `--yes` in ComposioHQ/composio#4817, +// issue #4816; the list's ids and names in #4809; the login's poll and the +// organizations' dates not yet), // this file is removed and the recipe returns to Composio's installer and its // latest stable release, as before 2026-10-13. -const version = "0.4.3-lazurio.2"; +const version = "0.4.3-lazurio.3"; /** The fork's commit whose `install.sh` runs: the release's own commit, the - * squash of Lazurio/composio#3 into `lazurio/cli-0.4.3`. Its installer is + * squash of Lazurio/composio#4 into `lazurio/cli-0.4.3`. Its installer is * byte for byte the one of the merge of #2, strict for this source and - * checking `COMPOSIO_INSTALL_SHA256`: #3 changed the CLI, not the installer. */ -const installerCommit = "e52fc87e7eaf07b7d83e0b50a8dd0c392d215294"; + * checking `COMPOSIO_INSTALL_SHA256`: #3 and #4 changed the CLI, not the + * installer (compared 2026-10-26). */ +const installerCommit = "5bcde7de8dc78681e8d44262fcf134afab19e091"; export const composioBuild = Object.freeze({ /** The fork whose GitHub releases publish the build: the assets @@ -58,18 +63,18 @@ export const composioBuild = Object.freeze({ installer: `https://raw.githubusercontent.com/Lazurio/composio/${installerCommit}/install.sh`, /** The SHA-256 of each target's release archive (`composio-.zip`, * as the release's `checksums.txt` lists them and as the downloaded - * archives hash, read 2026-10-19). The pinned installer takes it as + * archives hash, read 2026-10-26). The pinned installer takes it as * `COMPOSIO_INSTALL_SHA256` and refuses any other archive before it * extracts or replaces anything: the commit pins the script, these pin the * bytes it installs (Pablo's review of #395). */ archives: Object.freeze({ "linux-x64": - "a120ea54a9dfa6d59c89a68ed1a7db3149bc5a11f9dfa77db375a7d1b866144b", + "c48497ea9b691933575ab1edc6803b5eceb65d4e2fc32be61ce4803d07603027", "linux-arm64": - "86dee42fcda13d924843285f8da09afae001e0a357b51bdc863ff30695a9e27b", + "e8404f46d43ca537deb31658d30588e479b46553e02182b8d041db145a5734bc", "darwin-x64": - "119e76e31def9783cb3173c1665b50769d79f823294524c2549ddf883fe6a1a2", + "7c11326f54ac849a48bc9332032659edcda845e6334c179e89a0731a129fbc13", "darwin-arm64": - "f422bd6fc74aec9c89506dd37ca3efe529ab26210c1bd843bc4e6acb2d2fe26f", + "bc757402c3d304acdd46f7bfea435565a5cf538a16c3c0fea9bfa398c1938cb5", }), }); diff --git a/src/tools/login.ts b/src/tools/login.ts index aad10213..a0ab2cf9 100644 --- a/src/tools/login.ts +++ b/src/tools/login.ts @@ -185,6 +185,9 @@ export type ComposioOrganization = Readonly<{ id: string; name: string; current: boolean; + /** When Composio created it (ISO 8601), where the CLI prints it: Lazurio's + * build from 0.4.3-lazurio.3 on (decision F42, addendum of 2026-10-26). */ + createdAt?: string; }>; export type ComposioOrganizationsResult = | Readonly<{ @@ -1687,7 +1690,11 @@ export function createLoginSessions(environment: LoginEnvironment) { export type LoginSessions = ReturnType; // `composio orgs list` prints one JSON array of `{ id, name, -// is_selected_global_org }` on stdout when stdout is not a terminal. +// is_selected_global_org }` on stdout when stdout is not a terminal; Lazurio's +// build adds each one's `created_at` (decision F42, addendum of 2026-10-26). +// A date that is missing or not a date is left out, never a reason to refuse +// the list: then the account's default organization is not told, and the +// person's choice stands. export function parseComposioOrganizations( stdout: string, ): readonly ComposioOrganization[] | undefined { @@ -1710,6 +1717,7 @@ export function parseComposioOrganizations( const { id, name, + created_at: created, is_selected_global_org: selected, } = item as Record; const label = signInLabel(name); @@ -1720,7 +1728,18 @@ export function parseComposioOrganizations( organizations.some((entry) => entry.id === id) ) return undefined; - organizations.push({ id, name: label, current: selected === true }); + const createdAt = + typeof created === "string" && + created.length <= 64 && + !Number.isNaN(Date.parse(created)) + ? created + : undefined; + organizations.push({ + id, + name: label, + current: selected === true, + ...(createdAt === undefined ? {} : { createdAt }), + }); } return organizations; } diff --git a/tests/first-run.test.ts b/tests/first-run.test.ts index 277dcbc8..fa50963f 100644 --- a/tests/first-run.test.ts +++ b/tests/first-run.test.ts @@ -612,6 +612,22 @@ test("GitHub from the Tools reading of the profile shown", () => { for (const state of ["missing", "ambiguous", "elsewhere"] as const) expect(composioFact(signedIn, named(state))).toBe("unfinished"); expect(composioFact(signedIn, { kind: "unverified" })).toBe("unfinished"); + // A person's own Environment: done where the account's default + // organization is the current one (addendum of 2026-10-26). + expect( + composioFact(signedIn, { + kind: "default", + name: "jana_workspace", + state: "matches", + }), + ).toBe("connected"); + expect( + composioFact(signedIn, { + kind: "default", + name: "jana_workspace", + state: "elsewhere", + }), + ).toBe("unfinished"); // Signed out, the organization does not matter. expect( composioFact( diff --git a/tests/fixtures/fake-composio.ts b/tests/fixtures/fake-composio.ts index f634b33d..1b30cf33 100644 --- a/tests/fixtures/fake-composio.ts +++ b/tests/fixtures/fake-composio.ts @@ -212,7 +212,9 @@ if (command === "whoami") { // `orgs list` prints its JSON array on stdout when stdout is not a terminal // (the CLI's `ui.output`, after its human lines on stderr); `orgs switch -// --org-id` makes one current, which `whoami` then names. +// --org-id` makes one current, which `whoami` then names. An organization's +// `created_at` is printed where the state gives it, as Lazurio's build does +// from 0.4.3-lazurio.3 on. if (command === "orgs" && sub === "list") { requireAuth(); log("composio dev orgs list"); @@ -223,6 +225,9 @@ if (command === "orgs" && sub === "list") { (state.organizations ?? []).map((organization) => ({ id: organization.id, name: organization.name, + ...(organization.created_at === undefined + ? {} + : { created_at: organization.created_at }), is_selected_global_org: organization.name === current, })), ), diff --git a/tests/fixtures/integrations-world.ts b/tests/fixtures/integrations-world.ts index d3048235..84ed9533 100644 --- a/tests/fixtures/integrations-world.ts +++ b/tests/fixtures/integrations-world.ts @@ -61,8 +61,9 @@ export type FakeComposioState = { * where null. */ organization?: string | null; /** The account's organizations `orgs list` prints, `organization` the - * current one by its name; `orgs switch --org-id` makes one current. */ - organizations?: { id: string; name: string }[]; + * current one by its name; `orgs switch --org-id` makes one current. With + * `created_at`, as Lazurio's build prints it from 0.4.3-lazurio.3 on. */ + organizations?: { id: string; name: string; created_at?: string }[]; /** `orgs switch` fails, as a CLI that cannot reach Composio would. */ switchFails?: boolean; /** The key of the sign-in `login --no-wait` started, which `login --poll` diff --git a/tests/folder-tools.test.ts b/tests/folder-tools.test.ts index 6ebdf759..c8b91f6c 100644 --- a/tests/folder-tools.test.ts +++ b/tests/folder-tools.test.ts @@ -128,8 +128,8 @@ test("the catalog tiers and setup modes; the operator's other tools are not acti for (const locale of ["cs", "en"] as const) { const target = findTool("composio")?.activation?.installation[locale]; for (const part of [ - "0.4.3-lazurio.2", - "`@composio/cli@0.4.3-lazurio.2`", + "0.4.3-lazurio.3", + "`@composio/cli@0.4.3-lazurio.3`", "https://github.com/Lazurio/composio", "`lazurio tools install composio`", "`lazurio tools update composio`", diff --git a/tests/integrations-composio-organization.test.ts b/tests/integrations-composio-organization.test.ts index d4ca193f..72ffd5fe 100644 --- a/tests/integrations-composio-organization.test.ts +++ b/tests/integrations-composio-organization.test.ts @@ -9,6 +9,7 @@ import { import { composioOrganizationRule, composioOrganizationVerdict, + defaultComposioOrganization, } from "../src/integrations/composio-organization"; import { composioOrganizationName, @@ -52,7 +53,10 @@ import { bindings } from "./fixtures/machine-bindings"; // person creates "LazurioExampleOrganization"), made current by the // Launchpad; where the account has none, or more than one, nothing of // Composio counts and nothing connects through it. A personal Environment -// and a computer work in the one the person chose. +// works in the account's default organization, the one Composio made at the +// sign-up and so the oldest, made current by the Launchpad likewise (root +// decision 0162, addendum of 2026-10-13, point 2; addendum of 2026-10-26); +// a computer in the one the person chose. const cleanups: (() => Promise)[] = []; afterEach(async () => { @@ -213,7 +217,101 @@ test("the verdict over the account's organizations", () => { }); }); -test("the rule: an Organization's Environment by its Organization's name, unverified where it cannot be read, anything else chosen", async () => { +// The account's default organization (addendum of 2026-10-26): Composio +// marks none as the default and lists the newest first, so only the dates +// tell it, the oldest; without a date for each, or between two equally old, +// it is not guessed and the person's choice stands. +test("the account's default organization is its oldest, and never a guess", () => { + const dated = ( + current: string, + ...entries: (readonly [string, string | undefined])[] + ) => + entries.map(([name, createdAt], index) => ({ + id: `org_${index + 1}`, + name, + current: name === current, + ...(createdAt === undefined ? {} : { createdAt }), + })); + const own = { kind: "default" } as const; + // As Composio lists them: the newest first, the sign-up's last. + const listed = (current: string) => + dated( + current, + ["globex", "2026-10-13T18:00:00.000Z"], + ["acme", "2026-10-11T11:00:00.000Z"], + ["jana_workspace", "2026-09-09T09:00:00.000Z"], + ); + expect(defaultComposioOrganization(listed("acme"))?.name).toBe( + "jana_workspace", + ); + expect(composioOrganizationVerdict(own, listed("acme"))).toEqual({ + kind: "elsewhere", + organization: { + id: "org_3", + name: "jana_workspace", + current: false, + createdAt: "2026-09-09T09:00:00.000Z", + }, + current: "acme", + }); + expect(composioOrganizationVerdict(own, listed("jana_workspace"))).toEqual({ + kind: "matches", + organization: { + id: "org_3", + name: "jana_workspace", + current: true, + createdAt: "2026-09-09T09:00:00.000Z", + }, + }); + // A build that does not date them: the person's current one stands. + expect( + composioOrganizationVerdict( + own, + dated( + "acme", + ["acme", "2026-10-11T11:00:00.000Z"], + ["jana_workspace", undefined], + ), + ), + ).toEqual({ kind: "chosen", current: "acme" }); + // Two equally old: neither is taken for the default. + expect( + composioOrganizationVerdict( + own, + dated( + "acme", + ["acme", "2026-09-09T09:00:00.000Z"], + ["jana_workspace", "2026-09-09T09:00:00.000Z"], + ), + ), + ).toEqual({ kind: "chosen", current: "acme" }); + expect(composioOrganizationVerdict(own, [])).toEqual({ + kind: "chosen", + current: null, + }); + // A full page of `orgs list` (the newest 50) may leave out the oldest + // ones, the default among them (Pablo's review of Lazurio/composio#4): + // not told; one fewer is the whole list. + const page = (count: number) => + dated( + "org_name_1", + ...Array.from( + { length: count }, + (_, index) => + [ + `org_name_${index + 1}`, + new Date(Date.UTC(2026, 9, 20) - index * 86_400_000).toISOString(), + ] as const, + ), + ); + expect(composioOrganizationVerdict(own, page(50))).toEqual({ + kind: "chosen", + current: "org_name_1", + }); + expect(defaultComposioOrganization(page(49))?.name).toBe("org_name_49"); +}); + +test("the rule: an Organization's Environment by its Organization's name, unverified where it cannot be read, a personal Environment by the account's default, a computer chosen", async () => { const folder = join(await parent(), "Lazurio"); await mkdir(folder, { recursive: true, mode: 0o700 }); // No Organization of that slug in the Folder: unverified, never the @@ -231,7 +329,7 @@ test("the rule: an Organization's Environment by its Organization's name, unveri { kind: "named", name: "Příklad s.r.o." }, ); expect(await composioOrganizationRule(folder, bindings.personal)).toEqual({ - kind: "chosen", + kind: "default", }); expect(await composioOrganizationRule(folder, null)).toEqual({ kind: "chosen", @@ -348,7 +446,11 @@ const gmail = { async function launchpad( composio: Partial, - rule: { kind: "named"; name: string } | { kind: "unverified" } | null, + rule: + | { kind: "named"; name: string } + | { kind: "default" } + | { kind: "unverified" } + | null, ) { const directory = await parent(); const world = await integrationsWorld(directory, { @@ -847,7 +949,162 @@ test("the sign-in dialog says in one sentence when its organization is there twi } }); -test("a personal Environment's sign-in dialog leaves the choice to the person", async () => { +// The sign-in lands in the organization open in Composio's dashboard, which +// is often one of an Organization, since the person creates those there +// (the Operator's report of 2026-10-26: a personal Environment showed the +// apps of a work organization). A personal Environment therefore makes the +// account's default organization current itself, as an Organization's +// Environment does with the one of its name (addendum of 2026-10-26). +test("a personal Environment works in the account's default organization: the sign-in dialog and the reading make it current, with nothing to choose", async () => { + const directory = await parent(); + const world = await integrationsWorld(directory, { + composio: { + accounts: [gmail], + organization: "acme", + organizations: [ + { id: "org_1", name: "acme", created_at: "2026-10-11T11:00:00.000Z" }, + { + id: "org_2", + name: "jana_workspace", + created_at: "2026-09-09T09:00:00.000Z", + }, + ], + }, + }); + cleanups.push(() => world.executor.stop()); + const app = await worldLaunchpad(directory, world, { + tools: ["composio"], + handover: { preset: "hosted-personal", machine: bindings.personal }, + }); + cleanups.push(() => app.close()); + const answer = await ( + await app.post("/api/tools/composio/organizations", {}) + ).json(); + expect(answer).toMatchObject({ + kind: "composio-organizations", + organization: { name: "jana_workspace", state: "matches", default: true }, + }); + expect((await readComposioState(world.home)).organization).toBe( + "jana_workspace", + ); + expect(organizationRule(answer)).toEqual({ + kind: "default", + name: "jana_workspace", + state: "matches", + }); + // The dialog: connected in it, and nothing to choose, since the next + // reading would make it current again. + expect(composioDialogView(answer, "op@example.com", messages("en"))).toEqual({ + kind: "connected", + account: "op@example.com", + organization: "jana_workspace", + choices: null, + }); + // Another one made current behind the reading (an agent's `lazurio tools + // composio-org switch`): Integrace's reading brings the default back, and + // its accounts and links are the default one's. + expect( + await ( + await app.post("/api/tools/composio/organization", { id: "org_1" }) + ).json(), + ).toMatchObject({ kind: "composio-organization-selected", id: "org_1" }); + const reading = (await ( + await app.get("/api/integrations?refresh=1") + ).json()) as IntegrationsOverview; + expect(reading.composio.organization).toEqual({ + name: "jana_workspace", + state: "matches", + default: true, + }); + expect(reading.sources.composio).toBe("ok"); + expect(reading.composio.apps).toBe( + "https://dashboard.composio.dev/jana_workspace/~/connect/apps", + ); + expect(composioOrganizationLine(reading, messages("cs"))).toBeNull(); + expect((await readComposioState(world.home)).organization).toBe( + "jana_workspace", + ); + expect(switches(await composioCalls(world.home))).toEqual([ + "org_2", + "org_1", + "org_2", + ]); +}); + +// The default organization that cannot be made current: nothing of +// Composio counts, as where an Organization's one cannot (point 4), and the +// person and the agents read its name as Composio names it. +test("a personal Environment whose default organization cannot be made current: nothing of Composio counts, and its name is Composio's own", async () => { + const directory = await parent(); + const world = await integrationsWorld(directory, { + composio: { + accounts: [gmail], + organization: "acme", + switchFails: true, + organizations: [ + { id: "org_1", name: "acme", created_at: "2026-10-11T11:00:00.000Z" }, + { + id: "org_2", + name: "jana_workspace", + created_at: "2026-09-09T09:00:00.000Z", + }, + ], + }, + }); + cleanups.push(() => world.executor.stop()); + const app = await worldLaunchpad(directory, world, { + tools: ["composio"], + handover: { preset: "hosted-personal", machine: bindings.personal }, + }); + cleanups.push(() => app.close()); + const answer = await ( + await app.post("/api/tools/composio/organizations", {}) + ).json(); + expect(answer).toMatchObject({ + organization: { name: "jana_workspace", state: "elsewhere", default: true }, + }); + expect(composioDialogView(answer, "op@example.com", messages("en"))).toEqual({ + kind: "problem", + message: + "Composio could not be switched to your organization “jana_workspace”.", + }); + const reading = (await ( + await app.get("/api/integrations?refresh=1") + ).json()) as IntegrationsOverview; + expect(reading.sources.composio).toBe("organization"); + expect(reading.composio.organization).toEqual({ + name: "jana_workspace", + state: "elsewhere", + default: true, + }); + expect(reading.apps.find((item) => item.id === "gmail")?.accounts).toEqual( + [], + ); + expect(composioOrganizationLine(reading, messages("en"))).toBe( + "Composio could not be switched to your organization “jana_workspace”, so nothing connects through it now. Click Check connections to try again.", + ); + expect(integrationsText(reading)).toContain( + 'switched to the organization "jana_workspace"', + ); + const link = await app.post("/api/integrations/connect", { + app: "salesforce", + }); + expect(link.status).toBe(409); + expect(await link.json()).toEqual({ + kind: "blocked", + reason: "composio-organization", + }); + expect( + (await composioCalls(world.home)).some( + (call) => call[0] === "link" && !call.includes("--list"), + ), + ).toBe(false); +}); + +// A Composio CLI that does not date the organizations (Composio's own, or +// Lazurio's before 0.4.3-lazurio.3): which one is the default cannot be +// told, so the person's choice stands and the dialog offers it, as before. +test("a personal Environment whose CLI does not date the organizations leaves the choice to the person", async () => { const directory = await parent(); const world = await integrationsWorld(directory, { composio: { @@ -1107,6 +1364,54 @@ test("a link after the organization was switched away goes to this Environment's expect((await readComposioState(world.home)).organization).toBe("priklad"); }); +// The same live check on a personal Environment (addendum of 2026-10-26): +// a link after another organization was made current behind the reading +// goes to the account's default one, made current first. +test("a link on a personal Environment after the organization was switched away goes to the account's default one again", async () => { + const { world, app, read } = await launchpad( + { + organization: "acme", + organizations: [ + { id: "org_1", name: "acme", created_at: "2026-10-11T11:00:00.000Z" }, + { + id: "org_2", + name: "jana_workspace", + created_at: "2026-09-09T09:00:00.000Z", + }, + ], + }, + { kind: "default" }, + ); + expect((await read()).composio.organization).toEqual({ + name: "jana_workspace", + state: "matches", + default: true, + }); + expect( + await ( + await app.post("/api/tools/composio/organization", { id: "org_1" }) + ).json(), + ).toMatchObject({ kind: "composio-organization-selected", id: "org_1" }); + const answer = await app.post("/api/integrations/connect", { + app: "salesforce", + }); + expect(answer.status).toBe(200); + expect(await answer.json()).toMatchObject({ kind: "authorize" }); + const calls = await composioCalls(world.home); + expect(switches(calls)).toEqual(["org_2", "org_1", "org_2"]); + const back = lastIndex( + calls, + (call) => call[0] === "orgs" && call[1] === "switch", + ); + const link = calls.findIndex( + (call) => call[0] === "link" && call[1] === "salesforce", + ); + expect(back).toBeLessThan(link); + expect((await readComposioState(world.home)).organization).toBe( + "jana_workspace", + ); +}); + test("a link or a removal when the organization is gone since the reading happens in the person's current one, as the Operator decided", async () => { const { world, app, read } = await launchpad( { diff --git a/tests/integrations-page.test.ts b/tests/integrations-page.test.ts index e9b66fff..71055e60 100644 --- a/tests/integrations-page.test.ts +++ b/tests/integrations-page.test.ts @@ -1143,7 +1143,7 @@ test("the line about Composio's update: only where the reading offers it, busy u composioUpdated({ kind: "tool-updated", tool: "composio", - after: { version: "0.4.3-lazurio.2" }, + after: { version: "0.4.3-lazurio.3" }, }), ).toBe(true); for (const answer of [ diff --git a/tests/launchpad-curated.test.ts b/tests/launchpad-curated.test.ts index ee96ca53..adb6c578 100644 --- a/tests/launchpad-curated.test.ts +++ b/tests/launchpad-curated.test.ts @@ -736,7 +736,7 @@ echo "Installing Composio CLI $requested" expect(await composio()).toMatchObject({ installed: true, version: "0.3.1", - pinned: "0.4.3-lazurio.2", + pinned: "0.4.3-lazurio.3", }); expect( await opened.json("/api/tools/upgrade", { tool: "gh" }), @@ -755,17 +755,17 @@ echo "Installing Composio CLI $requested" tool: "composio", installer: { url: composioBuild.installer, - version: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", }, before: { version: "0.3.1" }, - after: { version: "0.4.3-lazurio.2" }, + after: { version: "0.4.3-lazurio.3" }, changed: true, }, }); expect(requests).toEqual([composioBuild.installer]); expect(await composio()).toMatchObject({ - version: "0.4.3-lazurio.2", - pinned: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", + pinned: "0.4.3-lazurio.3", }); // An Environment still on the previous pin, Lazurio's 0.4.3-lazurio.1: // not the build Lazurio pins now, and the same update brings it there. @@ -778,7 +778,7 @@ echo "Installing Composio CLI $requested" ); expect(await composio({ fresh: true })).toMatchObject({ version: "0.4.3-lazurio.1", - pinned: "0.4.3-lazurio.2", + pinned: "0.4.3-lazurio.3", }); expect( await opened.json("/api/tools/upgrade", { tool: "composio" }), @@ -787,11 +787,11 @@ echo "Installing Composio CLI $requested" body: { kind: "tool-updated", before: { version: "0.4.3-lazurio.1" }, - after: { version: "0.4.3-lazurio.2" }, + after: { version: "0.4.3-lazurio.3" }, changed: true, }, }); - expect(await composio()).toMatchObject({ version: "0.4.3-lazurio.2" }); + expect(await composio()).toMatchObject({ version: "0.4.3-lazurio.3" }); } finally { await opened.close(); } @@ -840,7 +840,7 @@ printf '#!/bin/sh\\necho %s\\n' "\${requested#@composio/cli@}" > "$HOME/.local/b status: 200, body: { kind: "tool-updated", changed: true }, }); - expect(await composio()).toMatchObject({ version: "0.4.3-lazurio.2" }); + expect(await composio()).toMatchObject({ version: "0.4.3-lazurio.3" }); } finally { release(); await opened.close(); diff --git a/tests/launchpad-tools.test.ts b/tests/launchpad-tools.test.ts index a5287acd..0a8ba73b 100644 --- a/tests/launchpad-tools.test.ts +++ b/tests/launchpad-tools.test.ts @@ -278,7 +278,7 @@ test.skipIf(process.platform === "win32")( version: "0.7.1", standardPath: false, // Lazurio's build of composio (decision F42 addendum (3)). - pinned: "0.4.3-lazurio.2", + pinned: "0.4.3-lazurio.3", prompt: toolPrompt("composio", "en") as string, }, { diff --git a/tests/tools-install.test.ts b/tests/tools-install.test.ts index daef17ba..9c26f080 100644 --- a/tests/tools-install.test.ts +++ b/tests/tools-install.test.ts @@ -683,24 +683,24 @@ echo "Installing Composio CLI $requested" // The tag and the installer's commit come from one release of the fork and // are pinned together; the installer's URL names the commit, so it is // immutable. -test("composio: the pinned build is Lazurio's 0.4.3-lazurio.2, installed by the fork's installer at a fixed commit", () => { +test("composio: the pinned build is Lazurio's 0.4.3-lazurio.3, installed by the fork's installer at a fixed commit", () => { expect(composioBuild).toEqual({ owner: "Lazurio", repository: "composio", - version: "0.4.3-lazurio.2", - tag: "@composio/cli@0.4.3-lazurio.2", - installerCommit: "e52fc87e7eaf07b7d83e0b50a8dd0c392d215294", + version: "0.4.3-lazurio.3", + tag: "@composio/cli@0.4.3-lazurio.3", + installerCommit: "5bcde7de8dc78681e8d44262fcf134afab19e091", installer: - "https://raw.githubusercontent.com/Lazurio/composio/e52fc87e7eaf07b7d83e0b50a8dd0c392d215294/install.sh", + "https://raw.githubusercontent.com/Lazurio/composio/5bcde7de8dc78681e8d44262fcf134afab19e091/install.sh", archives: { "linux-x64": - "a120ea54a9dfa6d59c89a68ed1a7db3149bc5a11f9dfa77db375a7d1b866144b", + "c48497ea9b691933575ab1edc6803b5eceb65d4e2fc32be61ce4803d07603027", "linux-arm64": - "86dee42fcda13d924843285f8da09afae001e0a357b51bdc863ff30695a9e27b", + "e8404f46d43ca537deb31658d30588e479b46553e02182b8d041db145a5734bc", "darwin-x64": - "119e76e31def9783cb3173c1665b50769d79f823294524c2549ddf883fe6a1a2", + "7c11326f54ac849a48bc9332032659edcda845e6334c179e89a0731a129fbc13", "darwin-arm64": - "f422bd6fc74aec9c89506dd37ca3efe529ab26210c1bd843bc4e6acb2d2fe26f", + "bc757402c3d304acdd46f7bfea435565a5cf538a16c3c0fea9bfa398c1938cb5", }, }); // Every target the recipe installs has its archive pinned. @@ -708,7 +708,7 @@ test("composio: the pinned build is Lazurio's 0.4.3-lazurio.2, installed by the expect(digest).toMatch(/^[0-9a-f]{64}$/); expect(pinnedInstaller("composio")).toEqual({ url: composioBuild.installer, - version: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", }); // gh and wacli install their latest release: nothing is pinned. for (const name of ["gh", "wacli", "neon", "unknown"]) @@ -736,14 +736,14 @@ test.skipIf(!posix)( ).toEqual({ kind: "installed", tool: "composio", - version: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", path: join(box.bin, "composio"), onPath: true, }); // The installer is the fork's at its pinned commit, never // composio.dev's. expect(source.requests).toEqual([ - "https://raw.githubusercontent.com/Lazurio/composio/e52fc87e7eaf07b7d83e0b50a8dd0c392d215294/install.sh", + "https://raw.githubusercontent.com/Lazurio/composio/5bcde7de8dc78681e8d44262fcf134afab19e091/install.sh", ]); const installer = seen.find((call) => call.command[0] === "/bin/sh"); expect(installer?.command[1]).toMatch( @@ -757,10 +757,10 @@ test.skipIf(!posix)( COMPOSIO_INSTALL_HELP: "0", COMPOSIO_GITHUB_OWNER: "Lazurio", COMPOSIO_GITHUB_REPO: "composio", - COMPOSIO_INSTALL_VERSION: "@composio/cli@0.4.3-lazurio.2", + COMPOSIO_INSTALL_VERSION: "@composio/cli@0.4.3-lazurio.3", // This target's archive, pinned (linux-x64 here). COMPOSIO_INSTALL_SHA256: - "a120ea54a9dfa6d59c89a68ed1a7db3149bc5a11f9dfa77db375a7d1b866144b", + "c48497ea9b691933575ab1edc6803b5eceb65d4e2fc32be61ce4803d07603027", }); // The private script is gone. expect( @@ -776,7 +776,7 @@ test.skipIf(!posix)( ).toEqual({ kind: "already-installed", tool: "composio", - version: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", path: join(box.bin, "composio"), }); expect(again.requests).toEqual([]); @@ -904,7 +904,7 @@ test.skipIf(!posix)( tool: "composio", stage: "verify", reason: "version-mismatch", - detail: "Installing Composio CLI @composio/cli@0.4.3-lazurio.2", + detail: "Installing Composio CLI @composio/cli@0.4.3-lazurio.3", fallback: "agent", }); expect( @@ -975,14 +975,14 @@ test.skipIf(!posix)( tool: "composio", installer: { url: composioBuild.installer, - version: "0.4.3-lazurio.2", + version: "0.4.3-lazurio.3", }, changed: true, - output: "Installing Composio CLI @composio/cli@0.4.3-lazurio.2", + output: "Installing Composio CLI @composio/cli@0.4.3-lazurio.3", }); if (stock.kind !== "tool-updated") throw new Error("not updated"); expect(stock.before.version).toBe("0.4.3"); - expect(stock.after.version).toBe("0.4.3-lazurio.2"); + expect(stock.after.version).toBe("0.4.3-lazurio.3"); expect(stock.after.path).toBe(join(box.bin, "composio")); expect("command" in stock).toBe(false); expect(await readFile(join(home, "user_data.json"), "utf8")).toBe(signIn); @@ -998,7 +998,7 @@ test.skipIf(!posix)( expect(older).toMatchObject({ kind: "tool-updated", changed: true }); if (older.kind !== "tool-updated") throw new Error("not updated"); expect(older.before.version).toBe(previous); - expect(older.after.version).toBe("0.4.3-lazurio.2"); + expect(older.after.version).toBe("0.4.3-lazurio.3"); expect(await readFile(join(home, "user_data.json"), "utf8")).toBe( signIn, ); @@ -1027,9 +1027,9 @@ test.skipIf(!posix)( expect(await update(other.fetcher)).toMatchObject({ kind: "tool-update-failed", tool: "composio", - installer: { version: "0.4.3-lazurio.2" }, + installer: { version: "0.4.3-lazurio.3" }, output: - "verify: version-mismatch\nInstalling Composio CLI @composio/cli@0.4.3-lazurio.2", + "verify: version-mismatch\nInstalling Composio CLI @composio/cli@0.4.3-lazurio.3", }); // A composio elsewhere on PATH stays the operator's: no second copy, // nothing downloaded. @@ -1081,7 +1081,7 @@ test.skipIf(!posix)( }; const text = await runToolsCommand(["update", "composio"], context); expect(text.code).toBe(0); - expect(text.text).toBe("composio: 0.4.3 -> 0.4.3-lazurio.2"); + expect(text.text).toBe("composio: 0.4.3 -> 0.4.3-lazurio.3"); const json = await runToolsCommand( ["update", "composio", "--json"], context, @@ -1089,7 +1089,7 @@ test.skipIf(!posix)( expect(json.code).toBe(0); expect(JSON.parse(json.text)).toMatchObject({ kind: "tool-updated", - installer: { version: "0.4.3-lazurio.2" }, + installer: { version: "0.4.3-lazurio.3" }, changed: false, }); // A failed update exits 1 and says why. diff --git a/tests/tools-login.test.ts b/tests/tools-login.test.ts index 811b89e8..fa69a6af 100644 --- a/tests/tools-login.test.ts +++ b/tests/tools-login.test.ts @@ -163,6 +163,27 @@ test("composio organizations are read only in their exact form", () => { ).toBeUndefined(); }); +// Lazurio's build prints each organization's `created_at` (decision F42, +// addendum of 2026-10-26): kept where it is a date, left out where it is +// missing or not one, never a reason to refuse the list. +test("composio organizations keep the date Lazurio's build prints, and only a date", () => { + expect( + parseComposioOrganizations( + '[{"id":"org_1","name":"First","created_at":"2026-10-11T11:00:00.000Z","is_selected_global_org":true},{"id":"org_2","name":"Second","created_at":"yesterday","is_selected_global_org":false},{"id":"org_3","name":"Third","created_at":7,"is_selected_global_org":false},{"id":"org_4","name":"Fourth","is_selected_global_org":false}]', + ), + ).toEqual([ + { + id: "org_1", + name: "First", + current: true, + createdAt: "2026-10-11T11:00:00.000Z", + }, + { id: "org_2", name: "Second", current: false }, + { id: "org_3", name: "Third", current: false }, + { id: "org_4", name: "Fourth", current: false }, + ]); +}); + test.skipIf(!keygen)( "gh: the device code and page as the challenge, no clipboard, signed in when the probe confirms", async () => { diff --git a/tests/tools-view.test.ts b/tests/tools-view.test.ts index c768a9b1..ec3191c1 100644 --- a/tests/tools-view.test.ts +++ b/tests/tools-view.test.ts @@ -283,7 +283,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only path: "/home/o/.local/bin/composio", realPath: "/home/o/.composio/composio", standardPath: true, - pinned: "0.4.3-lazurio.2", + pinned: "0.4.3-lazurio.3", } as const; const stock = tool({ ...installed, version: "0.4.3" }); for (const locale of ["en", "cs"] as const) { @@ -292,7 +292,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only state: "attention", headline: copy.toolsInstalled.replace("{version}", "0.4.3"), path: "/home/o/.local/bin/composio → /home/o/.composio/composio", - notes: [copy.toolsNotPinned.replace("{pinned}", "0.4.3-lazurio.2")], + notes: [copy.toolsNotPinned.replace("{pinned}", "0.4.3-lazurio.3")], fix: false, }); expect(updateAction(stock, copy)).toEqual({ @@ -304,7 +304,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only const previous = tool({ ...installed, version: "0.4.3-lazurio.1" }); expect(toolStatusView(previous, copy, true)).toMatchObject({ state: "attention", - notes: [copy.toolsNotPinned.replace("{pinned}", "0.4.3-lazurio.2")], + notes: [copy.toolsNotPinned.replace("{pinned}", "0.4.3-lazurio.3")], }); expect(updateAction(previous, copy)?.label).toBe(copy.toolsUpdateAction); // Signed in or not: the update keeps the sign-in. A version that could @@ -321,14 +321,14 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only // Environment does not offer, one the Organization does not allow, and // a tool an agent sets up: no "Update". for (const other of [ - tool({ ...installed, version: "0.4.3-lazurio.2" }), + tool({ ...installed, version: "0.4.3-lazurio.3" }), tool({ installed: true, version: "0.4.3", path: "/home/o/.local/bin/composio", standardPath: true, }), - tool({ pinned: "0.4.3-lazurio.2" }), + tool({ pinned: "0.4.3-lazurio.3" }), tool({ ...installed, version: "0.4.3", standardPath: false }), tool({ ...installed, version: "0.4.3", offered: false }), tool({ @@ -341,7 +341,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only expect(updateAction(other, copy)).toBeNull(); expect( toolStatusView( - tool({ ...installed, version: "0.4.3-lazurio.2" }), + tool({ ...installed, version: "0.4.3-lazurio.3" }), copy, true, ), @@ -352,7 +352,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only { kind: "tool-updated", tool: "composio", - after: { version: "0.4.3-lazurio.2" }, + after: { version: "0.4.3-lazurio.3" }, changed: true, }, "composio", @@ -363,7 +363,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only reload: false, message: copy.toolsUpdatedNow .replace("{name}", "composio") - .replace("{version}", "0.4.3-lazurio.2"), + .replace("{version}", "0.4.3-lazurio.3"), }); expect( updateOutcome( @@ -422,7 +422,7 @@ test("a tool that is not the build Lazurio pins says so, and offers Update only ]).toEqual(["Update", "Aktualizovat"]); // The page takes the pin from the server's answer, only as text. const answer = { ...overview([stock]) }; - expect(parseToolsOverview(answer)?.tools[0]?.pinned).toBe("0.4.3-lazurio.2"); + expect(parseToolsOverview(answer)?.tools[0]?.pinned).toBe("0.4.3-lazurio.3"); expect( parseToolsOverview({ ...answer, tools: [{ ...stock, pinned: 4 }] }), ).toBeNull();