Skip to content

F20: first installation in one command — the executable verifies its release, install.sh for a new computer, prepared prompt for a non-standard installation - #62

Merged
immakermatty merged 3 commits into
mainfrom
claude/DEV-6626-install-script
Sep 28, 2026
Merged

immakermatty merged 3 commits into
mainfrom
claude/DEV-6626-install-script

Conversation

@immakermatty

@immakermatty immakermatty commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Why

Decisions of the Principal on 2026-09-28: the first installation of Lazurio on a new computer is one command, an install script served from https://lazurio.ai/install; it installs the CLI and the Launchpad, afterwards lazurio update takes over. Lazurio is installed exactly the standard way everywhere; a deviation is reported and an agent straightens it by the manual.

The existing install.sh verified the release attestation only when gh was on the computer. A customer's new laptop has no gh, so there the download was trusted on HTTPS alone.

What changes (decision F20)

  • The executable verifies its release before a first installation. install.sh downloads the executable, the manifest and the attestation into a private directory, holds the executable against the manifest's SHA-256 before running it, and runs <executable> install --verify-release <dir>. The executable uses the same verification as lazurio update (now one shared function) and refuses to install otherwise. A refusal leaves nothing behind.
  • The staged path is unchanged: lazurio install --base from a pinned file, as the Machines role uses it, needs no network. Both paths are named in the code and the docs.
  • install.sh rewritten for a computer with only curl and sha256sum or shasum; every download and every redirect is held to HTTPS (--proto '=https' --proto-redir '=https'). There is no wget fallback: no wget family can refuse a redirect from HTTPS to HTTP in a way that could be tested here, so a computer without curl is told which package to install and the script stops before any request. Otherwise: plain messages, no sudo, no shell profile edits, refusal of unsupported platforms with the supported ones named, the next command printed at the end.
  • lazurio install prompt: the prepared prompt for an agent that straightens a non-standard installation (what the standard is, what was found, what the agent may do, what only on the operator's instruction, how success is proven). Czech and English. Read-only.
  • Docs: "First installation" in docs/update.md with the contract for the website route, README, decision F20.

The trust chain, stated plainly

Step What it proves What it does not
HTTPS to lazurio.ai which script is served who wrote it
Digest against the manifest, before execution the download is intact and belongs to that manifest who published the manifest
The executable's own verification an honest executable refuses a release that release.yml did not publish nothing against an executable replaced together with the script
gh attestation verify, when a signed-in gh exists the same attestation, by a program that did not arrive with the download —

The first installation stays trust on first use. Every later update is authenticated by the installed product.

What does not change

.github/workflows/release.yml: attaching and attesting install.sh as a release asset is a separate pull request. Until it merges, no release carries the asset and the interim address is the script of this repository.

src/launchpad/*, src/tools/*, the instruction template revision, the update contract.

Not verified, stated plainly

  • Nothing ran against a real release of GitHub and the real Sigstore; tests use a throwaway test Sigstore and fixtures. The first real proof is the next release candidate.
  • The Rosetta detection ran only against a shim.
  • The script of the repository installs the latest release; until a release built with --verify-release exists, that older executable refuses the call and nothing is installed.
  • Releases before this change have no install.sh asset and do not know --verify-release.
  • https://lazurio.ai/install is not deployed; the website route is a separate change in another repository.

Open questions for the Principal (not blocking)

  1. The script refuses to run as root. Keep for laptops; allow servers later?
  2. The default Folder of a workstation is ~/Lazurio.

Verification

bun run check on this head, run by the delegating agent after merging main: see the comment below.

Refs #50

🤖 Generated with Claude Code

…release, install.sh for a new computer, prepared prompt for a non-standard installation

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@immakermatty

Copy link
Copy Markdown
Contributor Author

Independent bun run check on aa63155 (macOS arm64): 758 pass, 4 skip (systemd tests), 0 fail; public guard PASS; standalone smoke PASS.

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty Exact-head review of aa63155 (base 0fa47cf). REQUEST_CHANGES; no merge.

[P1] install.sh:139-145, 91-125 — The new wget-only path follows redirects with wget -q -O without refusing HTTPS -> HTTP (docs/update.md:222-225 explicitly acknowledges this). On a computer without signed-in gh, a plaintext hop can supply both a forged manifest and matching executable. The script digest comparison then succeeds and executes that untrusted executable before its self-verification; malicious bytes can simply skip --verify-release. Thus this path does not preserve the stated HTTPS first-use boundary. This is not a request to pin the manifest digest. Please fail closed on a downgrade, or drop the wget-only fallback until every redirect can be constrained to HTTPS; add a negative redirect test. The existing wget shim tests only copy files and cannot detect protocol downgrade.

[CI gate] On this exact head, Platform checks: Ubuntu SUCCESS, macOS FAILURE (tests/cli.test.ts default 5,000 ms timeout, 756 pass / 5 skip / 1 fail / 1 unhandled error); Macroscope SKIPPED. The independent macOS bun run check comment reports 758 pass / 4 skip, but does not turn the live failed check green. In my isolated clone, bun install --frozen-lockfile, focused serial tests with 20 s timeout (27 pass), lint, typecheck, and git diff --check passed; an initial default-timeout focused run also hit a 5 s test timeout. Please repair/rerun the macOS check and obtain green exact-head checks.

Other reviewed boundaries: verifyOwnRelease precedes install-base writes and checks version, commit, artifact size/digest and the same attestation routine used by update; refused fixture verifications left no installation. The staged Machines install --base path is intentionally offline and unchanged in the core. curl constrains redirect protocols; the script uses private temporary storage with cleanup and does not call sudo or edit profiles. No claim of a real GitHub release / Sigstore end-to-end proof, or native wget/Rosetta execution.

immakermatty and others added 2 commits September 28, 2026 13:22
…allback

Review of pull request 62: with wget -q -O the script followed redirects by
wget's rules and could not refuse a downgrade to plain HTTP. A plaintext hop
could then supply a forged manifest and a matching executable; the digest
comparison would pass and the executable would start before its own
attestation check, which malicious bytes skip. GNU wget 1.x cannot restrict
the scheme of a redirect, and following redirects by hand would have to be
right for GNU wget, wget2 and BusyBox wget alike without a way to prove it,
so the wget path is removed rather than widened: a computer with only wget
is told how to install curl and nothing is requested. The latest lookup
already refuses anything but the exact-tag HTTPS URL of the repository; a
test now names the http:// case.

The curl shim of the tests honours --proto-redir like curl 8 (its refusal
text observed against a loopback TLS server redirecting to plain HTTP) and
serves a forged manifest and executable over an http:// hop; the new test
proves the shim would hand them out without the flag, and that the script
stops at the first download, runs nothing and installs nothing. The tests
run under LAZURIO_TEST_SHELL (/bin/sh by default; also /bin/dash and
bash --posix). docs/update.md states the curl-only trade-off; F20 gains the
rejected wget alternative.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
It compiles src/cli.ts and runs the binary about a dozen times. On the
hosted runners it took 2.0 to 4.4 s on main as well (Ubuntu 4.35 s and
macOS 4.01 s on 0fa47cf), against bun's default of 5 s; on this branch the
macOS runner hit 5.0 s. Locally main and this branch take the same time
(five alternating runs each: 2.07-2.23 s and 2.12-2.25 s), so the slice
did not slow it; it is runner load. The "unhandled error" of that run was
the same timeout: bun killed the abandoned test's child (exit 143) and the
test body's next assertion failed after the test had ended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@agentrozjedemeai agentrozjedemeai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@immakermatty Exact-head re-review: APPROVED for b2ace73 against main 0fa47cf. No merge performed.

The prior HTTPS downgrade finding is addressed: install.sh now requires curl, constrains initial transfers and followed redirects to HTTPS, and rejects an http:// latest-tag redirect. The wget-only route is gone. The negative fixture proves a forged matching manifest/executable would be served without --proto-redir and is rejected before execution with it; HOME and TMPDIR remain clean. The compiled CLI test has an explicit 30 s timeout.

Exact-head required Platform checks: ubuntu-24.04 SUCCESS and macos-14 SUCCESS (run 36415402131, SHA verified); optional Macroscope SKIPPED. Zero unresolved review threads. In an isolated clean clone, install-script and CLI focused tests passed 12/12 with a 30 s local test override, install-script tests passed 10/10 under /bin/dash and /bin/bash --posix, lint/typecheck and diff --check passed. One initial local default-5-s run timed out in a different install-script test under host load; it passed on rerun with 30 s override. Non-blocking follow-up: give this other slow test an explicit timeout if this recurs in CI. This does not claim a real GitHub release/Sigstore end-to-end installation or a deployed lazurio.ai/install route.

@immakermatty
immakermatty merged commit 005632a into main Sep 28, 2026
3 checks passed
@immakermatty
immakermatty deleted the claude/DEV-6626-install-script branch September 28, 2026 11:50
immakermatty added a commit that referenced this pull request Sep 28, 2026
…ee times (#70)

The attestation test of install.sh runs the fixture installer with a signed-in
gh, a signed-out gh and a failing attestation, each a real process, and timed
out at bun's 5 s default under a load average of five (seen in #62's review
and in the author's check of #69). It gets the 30 s the CLI tests already use.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
agentrozjedemeai pushed a commit that referenced this pull request Oct 6, 2026
…hem (Anička, DEV-6645) (#226)

* Shell: the design system re-vendored at fdcc9b4 (three radii, one floating shadow)

src/shell/vendor/lazurio/tokens.css is copied byte for byte from
HumanAndMachine-ai/design-system-lazurio fdcc9b4 (#62). Since the previous
pin 5bbc1f7 (#54) only this file of the vendored ones changed, in #58 and
#59 (Anička, 2026-10-04): the comment on the radii now names three radii,
one per kind of thing, and --lz-shadow-float (0 8px 24px, ink at 10 %) is
new, the one shadow of a layer floating above the page. The logo symbol,
the stones and Iconoir's licence are byte for byte as before.

src/shell/vendor/README.md names the new source commit, what changed and
the new SHA-256; tests/apps-view.test.ts ("every vendored file has the hash
its README records") passes against it. Nothing reads the new token yet;
the shell's styles take it in the next commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Shell: the rail and the Environment picker as Anička revised the wireframe (DEV-6645)

The elements follow the shell wireframe at prototypes-lazurio 1acd615 and
the design system at fdcc9b4 (prototypes-lazurio #11, #14, #15, #17;
design-system-lazurio #57, #58, #59, #62).

The rail is one grid step, 64 px instead of 72, and sets
--lazurio-rail-width to 64px: 40 px items with radius-md around 32 px marks
with radius-sm, concentric corners, an item under the pointer brings the
quiet surface without changing its shape, smaller icons (16 px), "+" a
32 px dashed square, the account's photo 28 px, dividers 24 px. The active
space keeps B3; its spread shadows now fill exactly the 40 px item. Your
personal monogram is a mark on the raised colour with a hairline, never an
ink block, in the rail and in the picker. Without roles the rail stands on
paper like the app's column.

Every radius is one of the three tokens; the list under the picker, the
⌘⇧E dialog and the rail's label carry --lz-shadow-float with a hairline,
and the switch's active tab a hairline instead of its drop shadow.

The picker's list is one line per Environment of the space (switcherList in
view.ts): no Organization head and no title, a search field only past seven
Environments, only "Všechny Organizace" at its foot; widened, the field and
every space by its title, still without heads and without a foot. The
current row has the check, and "tady jsi" is for screen readers only.
⌘⇧E keeps the Organization's heads above the same rows and its key hints.
The keys (switcherKey) work with or without the field: arrows wrap, Home
and End outside the field, Enter, Escape; without the field the focus lands
on the current Environment, else the first, and moves with the keys and
the pointer. The Launchpad page's fallbacks of --lazurio-rail-width are
64px.

Tests: tests/shell-picker.test.ts pins the list, the threshold, the foot,
the jump's heads, the keys and the rail's width; the suites that asserted
the head in the picker now assert it in the jump only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Launchpad: the Organization's name in the Apps head opens its Dashboard (DEV-6645)

The picker no longer leads to an Organization's Dashboard (prototypes-lazurio
#17, design-system-lazurio #62): the Organization's name in the page's head
does, as the wireframe's .lp-head__link. appsHeading in apps-view.ts makes
the name a link to the Organization's page as the shell document names it
(the same lookup "Přístup k modulu" uses), titled "Dashboard Organizace" /
"Organization Dashboard", in the same window. The personal space ("Osobní"),
the home without a group and an Organization the document does not name
stay plain text. The link has the heading's colour and no underline, and is
underlined 2 px with a 4 px offset under the pointer.

Tested in tests/apps-view.test.ts: an Organization, one the shell document
does not name, one the Folder cannot read, the personal space and the home.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Docs: F36 addendum, the rail and the picker as the wireframe revised them (DEV-6645)

docs/decisions.md records the addendum of 2026-10-06, decided by Anička and
confirmed by Matěj: the 64 px rail with one corner family and the personal
monogram as a mark, three radii, one shadow for a floating layer, the
compact picker without the Organization's head, the keys with or without
the search field, ⌘⇧E keeping its heads, the Organization's name in the
Apps head opening its Dashboard, what the colour roles now colour, and that
no v1 document or interface name changes. It names what it supersedes in
the Organization-rail addendum (point 2), the colour addendum (point 1) and
the addendum of 2026-10-05 (point 6), which carry dated notes; what is left
as it is (the picker's button and gear, the icons' stroke) and where the
shell keeps its own rule; the alternatives; and how it was verified, with
the measurements against the wireframe at prototypes-lazurio 1acd615.

docs/launchpad-development.md describes the same in the target shell (the
rail, the picker, the selection, the Apps header), in the shell as built
("The look", the column head on an Organization's Dashboard, a space
without an Environment), in the colour roles table and the test list.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Shell: the picker and its gear as the design system draws them; icons at the wireframe's stroke of 1.6 (DEV-6645)

Matěj asked that the Environments and the Dashboard follow the wireframe as
it stands, so the column head now takes the design system's values
(design-system-lazurio fdcc9b4) as the wireframe draws it since
prototypes-lazurio #8, still its state at 1acd615:

- The picker is lz-picker: at least --lz-space-48 tall, --lz-space-8
  around, --lz-space-12 between its parts, radius-md; 52 px with its two
  lines (was 47). Its glyph is lz-picker__mark, --lz-space-32 with
  radius-sm, an Organization's mark or your monogram 24 px inside, the
  kind's icon filling it; the name --lz-size-nav, the line --lz-size-meta.
  The row is lz-picker-row, --lz-space-4 apart.
- The gear is lz-button--icon: a --lz-space-48 square with radius-sm in
  the picker's ink (was 40 px wide and muted). Under the pointer it keeps
  the quiet surface of the column's other controls: the design system's
  quiet button takes paper, which does not show on the paper column; under
  a host's roles both are --lazurio-hover.
- In a narrow host (@container max-width 160px) the picker and the gear
  stand as 48 px squares, one above the other; the 40 px override is gone.
- The glyph takes the picker's ink, so on a dark host without roles a
  kind's icon is light instead of near-black (issue #169).
- The switch's tabs take line-height normal, 49 px as the wireframe.

Every interface icon, src/shell/icons.ts and the Launchpad page's 32
inlined symbols, is drawn at stroke 1.6, as the wireframe's Icon.tsx has
done since before 1cbad15 (the provenance notes said 1.5); the notes now
cite 1acd615.

The F36 addendum gains point 5 (the picker and its gear) and loses the two
items it left as they were; its verification and colour-roles point say
what changed. No test pinned the old sizes.

Checked: bun run check (1463 pass, 7 skip, 0 fail). In Chrome against
the wireframe: the column head 191 x 52 beside the 48 px gear, the 32 px
glyph, 15 and 14 px text, the switch 49 px, icons at 1.6, all equal; in
T3 Code's dark sidebar at 208 and 264 px, its light one, MausBot's
Midnight, a dark host without roles and a 64 px host, nothing overflows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Docs: the F36 addendum's verification says the column head, not the page, was drawn narrow (DEV-6645)

The verification of the addendum on the rail and the picker said that
narrow widths were not exercised, while the column head was drawn in a
64 px host and at the 160 px limit. It now says that the column head was
checked in the Launchpad's preview too, and that the page at narrow window
widths was not exercised. The paragraph is rewrapped; nothing else changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants