Skip to content

feat: aeon init + credential manifest with drift test - #1158

Merged
aaronjmars merged 7 commits into
mainfrom
feat/aeon-init-credential-manifest
Oct 3, 2026
Merged

aaronjmars merged 7 commits into
mainfrom
feat/aeon-init-credential-manifest

Conversation

@aaronjmars

@aaronjmars aaronjmars commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

Setting up Aeon today takes about 11 manual steps, and two of them fail silently (a fork has Actions off, and without gh repo set-default secrets land on aeonfun/aeon). This PR makes it one command and gives every surface one list of "which secret runs which harness".

1. Credential manifest (harness-adapter/harnesses.json + new gateways.json)

  • Each adapter's rh-meta block now lists its credentials, most preferred first, in exactly the order scripts/resolve-harness.sh picks them: secret, kind (oauth_capture / oauth_token / api_key / oidc), auth_mode, label, prefix, get_url, login_cmd, aeon_cmd, cred_paths, expires / refresh, aux_secrets. Plus default_model.
  • The old auth summary is now derived from that list by the generator, so the two cannot disagree.
  • gateways.json (from a gw-meta block in adapters/claude.sh) lists every llm-gateway.sh provider in cascade order: secrets, prefixes, base URL, transport, where to get a key.
  • Drift fixed: grok no longer claims an OpenRouter fallback; pi lists ANTHROPIC_OAUTH_TOKEN; install commands and min_version match the real pins (kimi, vibe, fx, cursor, hermes were wrong); cursor with no key is labelled AUTH_MODE=none instead of openrouter (label only); hivemindos added to gateway-registry.ts.

2. Drift test (ci-tests)

scripts/tests/test_credential_manifest.sh (248 checks) holds the manifest to:

  • resolve-harness.sh: precedence parsed from the source and exercised (each credential alone gives its auth_mode; a more preferred one wins over a less preferred one); default_model.
  • harness-auth.ts: authSecrets order, OAuth secret, credPaths (read-only parse).
  • aeon.yml: every credential is bound in Resolve harness / Install harness CLI / Run.
  • OpenRouter entries exist only where install-harness.sh / resolve-harness.sh has a real OpenRouter route.
  • Install pins for all nine harnesses.
  • llm-gateway.sh (order, secrets, route arm, base URL host, sidecar or not) and gateway-registry.ts (slugs, labels, secrets, prefixes) + CLAUDE_AUTH_SECRETS.

ci-harnesses-json now diffs gateways.json too.

3. ./aeon init

Safe to re-run; every step checks first and prints what it fixed:

  1. gh installed, signed in, token has repo + workflow.
  2. Instance repo created from the template (not a fork). This folder is switched to it with aeonfun/aeon kept as upstream. The instance is fetched and checked out under a temporary remote first, and the remotes are renamed only after that worked, so an interrupted run can be re-run safely. Inside an existing instance, init repairs a branch that still tracks something other than origin. If that branch is still on the template history (template copies start with a fresh history), init checks out the instance branch instead, but only when that cannot lose work. It refuses uncommitted changes or unpushed commits on any branch, and checks this before it creates anything. --dir waits for the template copy to finish, clones it somewhere else and hands over to that copy's ./aeon init. It never adopts aeonfun/aeon, aaronjmars/aeon or any name that redirects.
  3. gh repo set-default to the instance.
  4. Actions enabled and allowed to open PRs. The default token permission and any existing allowed_actions choice are left as they are. Workflows GitHub left off (disabled_fork, disabled_inactivity) are turned back on.
  5. GH_GLOBAL from the gh token, only if it has repo + workflow.
  6. Model: a harness menu built from the manifest. It reuses the existing aeon auth flows (login capture, claude setup-token, pasted key, gateway keys) and points out the one-key OpenRouter option. If the chosen --harness is already connected, init only switches aeon.yml to it and never re-runs a login.
  7. Telegram: you paste the bot token, then tap a t.me/<bot>?start=<nonce> link. Init polls getUpdates without an offset, so no updates are consumed. On a 409, a backlog of 100 or more updates, or a timeout, it falls back to asking you to paste the chat id.
  8. Summary checklist, then an offer to start the dashboard.

Flags: --name, --private, --dir, --harness, --yes, --no-telegram, --no-dashboard, --dry-run. Without a terminal, every confirmation counts as "no" unless --yes is set, and init never starts a browser login or asks for a key.

commitAndPush (used by init and the dashboard to save aeon.yml) now pushes to origin by name and refuses when origin is the template; the edit stays on disk.

4. Smaller fixes

  • aeon auth --github and the dashboard GitHub connect now refuse a token without repo + workflow, or a fine-grained token whose scopes cannot be read. The route returns a 400 that says to set GH_GLOBAL to a classic PAT with repo + workflow. The docs and init note that GitHub revokes gh login tokens (after a year unused, or once more than 10 exist), so a long-lived instance should use a dedicated classic PAT.

  • aeon auth --harness grok works from the CLI.

  • aeon auth refuses to write secrets while gh points at the template.

  • bin/onboard (still read-only): checks the instance and gh default repo. The model check now uses the manifest, so gateway-only setups pass. An unknown harness is a warning and is checked as claude, which is what resolve-harness.sh runs. It reads the secret list once per run. --remote now dispatches heartbeat (the onboard skill it used to dispatch never existed). The help text says what that costs, and that heartbeat commits docs/status.md and stays quiet unless it finds something.

  • Root ./aeon launcher: on the first run it prints one line and runs a quiet npm ci, and shows the npm log only if the install fails. The banner box lines up for any port length (no emoji).

5. Docs

  • README quick start now leads with ./aeon init; the manual path stays as a fallback and says forks start with Actions off.
  • One PAT story across CONFIGURATION / mcp-oauth / harnesses / onboard: GH_GLOBAL is one classic PAT with repo + workflow (or the gh login token), and GH_SECRETS_PAT is optional.
  • docs/harnesses.md has a credential table generated from the manifest.
  • The aeon setup skill's Start mode now uses ./aeon init. The known drift between the two copies is unchanged.

How it was verified

  • bash apps/cli/test/init-sandbox.sh (new, runs in the ci-apps cli job): 43 checks against a fake gh backed by local bare repos, with no network. Its template copies get a fresh root commit, like GitHub. Covers the switch-over, an idempotent re-run with zero settings changes, repairing a branch left tracking the template, moving a folder that is still on the template history onto the instance (and refusing when that folder has local edits), resuming an interrupted switch-over, --harness on an already-connected harness (pushes harness: codex to the instance and leaves the template untouched), no --yes without a terminal, a dirty folder refused before the repo is created, and --dir hand-over plus refusing a non-empty folder or a file before anything is created. Turning off the repair step and the pre-create check made 4 checks fail.
  • lib/github-push.test.ts (new): commitAndPush pushes to origin while the branch tracks the template, and refuses when origin is the template.
  • bash scripts/tests/test_credential_manifest.sh: pass (266 checks). The registry parse no longer depends on key order (checked with a reordered, multi-line entry). Mutation check: 9 deliberate drifts (swapped precedence in resolve-harness.sh and in harness-auth.ts, default model, registry label, a gateway dropped from the registry, gateway order, fx pin, unbound workflow secret, a removed OpenRouter path). Every one failed the test.
  • Also passing: test_generate_harnesses_json, test_resolve_harness, test_install_harness, test_dashboard_model_choices, test_workflow_harness_choices, the adapter tests (grok, codex, cursor, hermes, pi), test_llm_gateway, test_all_secrets_allowlist, test_validate_readme_catalog, check-aeon-skill-sync.
  • Dashboard: npm run typecheck, npm run lint (0 errors; the 14 warnings were there before), npm test (234/234, including the new scope-check and push tests).
  • CLI: npm run typecheck and npm run lint, both clean.
  • shellcheck on every touched shell file; actionlint on the touched workflows (the only finding is an info-level one that was already in ci-tests).
  • aeon init end to end: run against a clone of the template, with a gh shim that lets reads through and blocks every GitHub write. It found an existing instance, switched the folder over (origin is the instance, upstream is aeonfun/aeon, main tracks origin), set the default repo, and detected the existing GH_GLOBAL, model and Telegram. A second run reported every step as already done. On a healthy live instance it makes zero PUTs.
  • --dry-run --yes from a fresh template clone shows the redirect guard (aaronjmars/aeon is refused) and the create path. Secrets are read only from the target repo (-R <target>), never from aeonfun/aeon.
  • bin/onboard checked on a live instance (14/14 pass, read-only), on a template clone (flags the instance), and with a gateway-only secret set (passes via the Bankr gateway).

Not verified live: creating a brand-new repo from the template, and the interactive key, Telegram and login prompts. These need a real account write or a TTY, and no throwaway repo was created.

Known gap, kept visible

XAI_API_KEY is not bound in aeon.yml's Run step env (it was dropped for per-skill least privilege). So the grok gateway only resolves for skills that list XAI_API_KEY in requires:. The grok harness is unaffected. The test holds this as a named known gap and fails once the binding comes back.

…ift test

Each adapter's rh-meta block now lists its credentials (secret, kind,
auth_mode, label, prefix, get_url, login_cmd, aeon_cmd, cred_paths,
expires/refresh, aux_secrets), most preferred first in exactly the order
scripts/resolve-harness.sh picks them, plus default_model. The old auth
summary is derived from that list by the generator, so the two cannot
disagree. The generator also writes harness-adapter/gateways.json from a
gw-meta block in adapters/claude.sh: every llm-gateway.sh provider in the
default cascade order with secrets, prefixes, base URL and transport.

Drift fixed on the way:
- grok no longer claims an OpenRouter fallback it does not have
- pi lists ANTHROPIC_OAUTH_TOKEN, which resolve-harness.sh already honours
- cli.install/min_version match the real pins (kimi/vibe had no install
  command; fx said 0.0.5 vs v0.0.12; cursor/hermes said latest)
- cursor with no CURSOR_API_KEY is labelled AUTH_MODE=none instead of
  openrouter (label only; install still fails closed)
- hivemindos added to the dashboard gateway registry

scripts/tests/test_credential_manifest.sh (wired into ci-tests) holds the
mirror to resolve-harness.sh (parsed and exercised), harness-auth.ts,
the aeon.yml env blocks, install pins, llm-gateway.sh and
gateway-registry.ts. ci-harnesses-json now diffs gateways.json too.
./aeon init takes a clone of aeonfun/aeon to a running instance and is safe
to re-run (each step checks first, then prints what it fixed):
1. gh installed, signed in, token has repo + workflow (gh auth login --web
   -s workflow / gh auth refresh)
2. instance repo created from the template, not a fork (forks start with
   Actions off); this folder is switched to it with aeonfun/aeon kept as
   upstream, refusing when that could lose work; --dir clones elsewhere.
   Never adopts aeonfun/aeon, aaronjmars/aeon or any name that redirects.
3. gh repo set-default to the instance
4. Actions enabled and allowed to open PRs; the default token permission
   and an existing allowed_actions choice are left as they are; workflows
   GitHub left off (disabled_fork / disabled_inactivity) are re-enabled
5. GH_GLOBAL from the gh token, only when it has repo + workflow
6. model: harness menu and credential list from harnesses.json, reusing
   the aeon auth flows (login capture, setup-token, pasted key, gateways)
7. Telegram: bot token, then a /start nonce deep link found by polling
   getUpdates without an offset; 409 or timeout falls back to manual paste
8. summary checklist, then optionally the dashboard

Also:
- captureGithubToken (dashboard route + aeon auth --github) refuses a
  token without repo + workflow, or a fine-grained one it cannot inspect
- aeon auth --harness grok (X login capture or xAI key)
- aeon auth refuses to write secrets while gh points at the template
- bin/onboard: instance + gh default checks, model check from the
  manifest (gateway-only setups pass), one secret list per run, --remote
  dispatches heartbeat (the onboard skill never existed)
- codex manifest note: the capture eventually expires
- README quick start: clone + ./aeon init; the manual path stays as a
  fallback and now says forks start with Actions disabled and why
  gh repo set-default is mandatory
- GH_GLOBAL = one classic PAT (repo + workflow) or the gh login token via
  ./aeon init / aeon auth --github; GH_SECRETS_PAT = optional, tried first
  by the rotating-login refresh paths, falls back to GH_GLOBAL. Same
  wording in CONFIGURATION.md, mcp-oauth.md, harnesses.md and bin/onboard
- docs/harnesses.md: credential precedence table generated from the
  manifest; fx/cursor rows now show their aeon auth --key path
- aeon setup skill (both copies, drift unchanged): Start mode runs
  ./aeon init
- CONTRIBUTING: new gateways also go in the gw-meta block
- apps/cli README: init + grok auth
…ompts

Review fixes for aeon init:
- Switch-over fetches and checks out the instance under a temporary remote
  first and renames remotes only after that worked, so an interruption can
  never leave main tracking the template; a re-run inside an instance
  repairs a branch that still tracks something other than origin.
- commitAndPush pushes to origin by name (HEAD:<branch>, pull --rebase from
  origin) and refuses when origin is the template; the edit stays local.
- --harness on an already-connected harness reports it and only switches
  aeon.yml; no login is re-run (grok rotates its capture on login).
- Without a terminal every confirm is "no" unless --yes, and no browser
  login or key prompt is started.
- --dir waits for the template copy to have aeon.yml, verifies the clone,
  removes only what it created, refuses a non-empty folder, and reports a
  failed hand-over.
- Dirty / local-commit checks run before the repo is created and count
  commits on every branch.
- Secret checks query the instance with -R, so a dry run from a template
  clone never reads aeonfun/aeon.
- Telegram: still no offset; a backlog of 100+ updates goes straight to
  the manual chat id.
- GH_GLOBAL scope refusals are a typed error, 400 from the route, with
  neutral wording; docs and init note that gh login tokens can be revoked
  and recommend a dedicated classic PAT for long-lived instances.
- bin/onboard: --remote help matches what heartbeat does; an unknown
  harness warns and checks claude, as resolve-harness.sh runs it.
- test_credential_manifest: key-order independent registry parse, guarded
  lookups, oauth capture parity check, clearer CLAUDE_AUTH_SECRETS message.

Tests: apps/cli/test/init-sandbox.sh (fake gh + bare repos, 34 checks, in
ci-apps) and lib/github-push.test.ts (commitAndPush against local repos).
- A template copy starts a fresh history, so when the folder's branch
  shares no commits with origin, init now checks out origin/<branch>
  (only with a clean tree and no unpushed commits) instead of only
  re-pointing @{u}; otherwise it stops with a hint.
- --dir is checked before the repo is created: a file or a non-empty,
  non-Aeon folder is refused (no ENOTDIR crash). A failed clone empties a
  folder that already existed instead of removing it.
- Clearer hints when the aeon-instance remote cannot be added and when the
  old origin cannot be moved (existing origin-previous).
- fake-gh `repo create` now makes a fresh root commit like GitHub; sandbox
  adds the fresh-history repair (and its dirty-folder refusal) and the
  --dir-on-a-file case.
- First run prints one line, "Installing dashboard dependencies (first run
  only)...", and runs `npm ci` (or `npm install` with no lockfile) with
  --no-audit --no-fund --loglevel=error. Output goes to a temp log that is
  shown (last 30 lines + path) only when the install fails, instead of
  pages of eslint peer warnings and a dev-only audit summary.
- The banner drops the two-column emoji and pads every row to the same
  width for any port length (checked with 4, 5 and 7 digit ports).
A live connect-check on a GitHub-hosted runner passed with a
CLAUDE_CODE_OAUTH_TOKEN from claude setup-token, so "frequently rejected"
overstated it. The note now says it can be rejected in some cases, to
confirm with Test connection, and to switch to an API key or a gateway key
(OpenRouter etc.) only if it shows zero usage.
@aaronjmars
aaronjmars merged commit 0a56409 into main Oct 3, 2026
17 checks passed
aaronjmars added a commit that referenced this pull request Oct 3, 2026
main tree equals the #1158 head, which this branch already contains.
@aaronjmars
aaronjmars deleted the feat/aeon-init-credential-manifest branch October 3, 2026 14:42
@aaronjmars aaronjmars mentioned this pull request Oct 3, 2026
5 tasks done
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.

1 participant