feat(installer): agent install + connect handshake (TIGER-89/90) - #2
Merged
Conversation
TIGER-89 asked for machine-readable outcomes; TIGER-90 asked how a fresh install hands a client a credential. Both are the same question — how does a non-human read this screen — so they get ONE answer rather than two mechanisms. Machine-readable state (TIGER-89) Every screen now carries <script type="application/json" id="tiger-install-state"> with step, status, next_step, expected fields, and a stable error slug plus the human message. Requirements additionally reports each check with ok/required/fix. status alone answers "did that work?": blocked = an unmet requirement, error = retryable, ok = only ever on finish. A JSON block rather than a <meta> because it carries structure (scope, URLs, field lists) that will not fit an attribute. The connect handshake (TIGER-90) A visible checkbox on the admin step, default off, optionally seeded by ?agent=1. Seeding is GET-only: on a POST the checkbox is the sole authority, so a crafted link pre-ticks a box the user can SEE and turn off. Verified: POST with ?agent=1 and the box unticked resolves to off. On finish, after do_create_owner() succeeds, mint a scoped token and enable /mcp, then display the key once. Two ordering decisions, both load-bearing: - Everything happens after the owner exists, so there is no window where MCP is reachable before an admin exists to revoke it. (Required by the ticket.) - Within that, mint BEFORE enabling. The ticket does not specify this half, and the reverse leaves MCP on with no credential if minting fails. The safe half-state is the one that grants nothing. Minting failure never fails the install — the site is live and the owner exists; only the convenience is lost, and the screen says so. Not ticking the box degrades to "enable at /mcp/admin, then reconnect" rather than silence, which is the majority path for a hand install. Scope is the curated starter set (Tiger_Mcp_Token::DEFAULT_MODULES), org-scoped, not read-only. tiger.api.discovery is deliberately untouched: MCP's tools/list already gives the client its typed surface, and publishing the OpenAPI document is a separate decision. No callback, and the code comments say why it must stay that way. The key is rendered on the installer's own screen and nowhere else; the only outbound calls remain the fixed GitHub release constants. A client that drove the install drove the browser and can read the finish page, so a callback would solve nothing while turning a shared installer link into credential phishing. README documents the whole contract — this is the seam a client builds against. Tested: 15 assertions on the checkbox, truthiness and state block; 7 scenarios on the seeding logic extracted from the shipped file so the test cannot drift from it; and the requirements step driven over HTTP with its state block parsed back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8p9pLJ3DFstG3xZuh2QgZ
This file is uploaded by hand to someone's shared host and runs ONCE, with no shell, no Composer and no chance to patch it mid-install. It had no CI at all, and it just grew a credential-minting path. Lint matrix 8.1-8.5. A parse error on a customer's PHP version is the worst failure this repo can ship: a blank page on their own server, mid-install, with no way to debug it. Shared hosting is the only target, so the matrix spans what a cPanel host actually offers rather than what we develop on. 61 assertions, no dependencies, same command locally as in CI: - invariants.php — one file with no dependencies of its own; NO callback, webhook, notify_url, redirect_uri, postback or return_url is ever read as input; outbound calls only reach the pinned release URLs; no shell functions; a checksum is required; it still self-deletes. - wizard.php — the agent checkbox is rendered, unticked by default, reversible, and never also a hidden input; the state block emits, parses, and a '<' in the payload cannot break out of the script element. - smoke.php — serves the installer over HTTP and reads its state block back. The seeding test lifts the real controller lines out of the shipped file at run time instead of copying them, so it cannot drift from the code it covers. Also guards INSTALLER_VERSION against the release tag on a v* push — the installer reports its own version and the README's download link is evergreen, so a mismatch ships an installer that lies about what it is. Mutation-tested rather than trusted because it went green: a callback field, a shell call, removed state-block escaping, the checkbox also riding in the hidden bag, seeding applied on POST, a required sibling file, and a default-ticked checkbox each fail a named assertion. The seeding mutation is the one that matters — it is the regression that would let a crafted ?agent=1 link survive the user un-ticking the box. One of those mutations appeared to survive on the first pass. It had not been applied — shell quoting ate the replacement. Verifying that before believing the result is the difference between a test gap and a harness bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8p9pLJ3DFstG3xZuh2QgZ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the two seams under TIGER-88:
TIGER-89 (install handshake) and
TIGER-90 (connect handshake).
One mechanism, not two
89 wants "success or the specific failure without human interpretation." 90 asks "how does the client
read the credential." That's the same question — how does a non-human read this screen — so it gets
one answer: a JSON state block on every screen, carrying the credential at finish.
statusalone answers "did that work?" —blocked(unmet requirement),error(retryable, with astable slug),
ok(only on finish). A<script>block rather than<meta>because it carriesstructure — scope, URLs, field lists — that doesn't fit an attribute.
89 — drivable was already true
Per the ticket's instruction to confirm before building: the wizard is plain HTML forms with no JS
requirement, so a browser-aware client can already fill and submit it. No new install path was
added — the ticket explicitly warns against one, and none was needed. The gap was purely that
outcomes had to be read out of prose. That's what this fixes.
90 — the shape the ticket settled
?agent=1a box the user can see and turn off. Verified:
POST ?agent=1+ unticked box → offdo_create_owner()succeeds: mint, enable, display oncetiger.api.discoveryleft alone/mcp/admin— the installer self-deletes, so it's the user's only chance tolearn the credential exists
One decision the ticket left open, made deliberately: within "after the owner exists", I mint
before enabling. The reverse leaves
/mcpon with no credential if minting fails. The safehalf-state is the one that grants nothing. Minting failure never fails the install.
No callback — and the code says why. The key renders on the installer's own screen and nowhere
else; the only outbound calls remain the fixed GitHub release constants. Verified there is no
callback/webhook/redirect_urifield anywhere.Still open for Astra
The ticket lists four convergence questions. This answers #1 (a stable JSON anchor — kinder to
every client than a DOM hook or plain text) and #3 (not ticked → degrade to "enable at
/mcp/admin, then reconnect", not a bare failure). #2 (is one token enough for open-endedauthoring — the curated set covers content and media but not themes/menus) and #4 (should a
loopback install get different defaults) are genuinely open and want Astra's input.
Tested
input, names
/mcp/admin; truthiness; state block emits, parses, escapes<, empty → nothingstatus)🤖 Generated with Claude Code
https://claude.ai/code/session_01L8p9pLJ3DFstG3xZuh2QgZ