diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8023b296..4d47566b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: pull_request: branches: - main + - release/0.3 workflow_dispatch: permissions: @@ -55,6 +56,9 @@ jobs: - name: Emit exact-head production package report run: npm run verify:production + - name: Smoke-test packed chat from a broad home directory + run: npm run smoke:packed-home + supply-chain: runs-on: ubuntu-latest timeout-minutes: 15 diff --git a/.github/workflows/release-truth.yml b/.github/workflows/release-truth.yml index b9afd7f4..36d21971 100644 --- a/.github/workflows/release-truth.yml +++ b/.github/workflows/release-truth.yml @@ -5,6 +5,9 @@ on: branches: - main - release/0.3 + pull_request: + branches: + - release/0.3 schedule: - cron: '17 11 * * *' workflow_dispatch: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16b5aa74..16096450 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,6 +87,9 @@ jobs: test "$("$prefix/bin/aether" --version)" = "${RELEASE_TAG#v}" "$prefix/bin/aether" --help > /dev/null + - name: Smoke-test exact tarball from a broad home directory + run: node dist/scripts/packed-home-chat-smoke.js "${{ steps.pack.outputs.tarball }}" + - name: Attest package provenance uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: diff --git a/README.md b/README.md index 39bd496f..e6aaf885 100644 --- a/README.md +++ b/README.md @@ -94,17 +94,16 @@ actions. ## Model stack -The dated public snapshot marks Aether Neo, DeepSeek V4, GPT-5.4/5.5/5.6, and -Claude 4.5/4.8/5 text entries as available at the catalogue level. That is not an -account entitlement: **`aether models` is the authoritative live result for the -signed-in account.** Kimi K2.6/K3 and Gemma 4 are catalogued but marked -**unavailable** in the current snapshot, so they are not presented here as usable. +The dated public snapshot below lists model entries and their publication +status. Listing is not an account entitlement: **`aether models` is the +authoritative live result for the signed-in account.** Availability can change +after the snapshot was generated. Local Ollama availability is independent of that hosted catalogue and depends on the models installed at the configured Ollama endpoint. -A dated, sanitized offline fallback snapshot is available as [HTML](docs/model-catalogue/index.html), [JSON](docs/model-catalogue/catalogue.json), and [Markdown](docs/generated/model-catalogue.md). It was generated at `2026-08-23T00:00:00.000Z` from Cloud public projection `model-catalogue-v1` with verified digest `sha256:80ba3ba1144d301e2cca407ceced74cb2b371f1da6e3982b87305ff12a3d4712`. Listed availability is not an account entitlement; use `aether models` while signed in. +A dated, sanitized offline fallback snapshot is available as [HTML](docs/model-catalogue/index.html), [JSON](docs/model-catalogue/catalogue.json), and [Markdown](docs/generated/model-catalogue.md). It was generated at `2026-09-27T21:15:38.529Z` from Cloud public projection `model-catalogue-v1` with verified digest `sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f`. Listed availability is not an account entitlement; use `aether models` while signed in. ## Core commands @@ -171,7 +170,7 @@ badge or `npm view aether-agents version` for the npm `latest` dist-tag, and |---|---:|---| | npm `latest` | [![npm latest](https://img.shields.io/npm/v/aether-agents?label=&color=14b8a6)](https://www.npmjs.com/package/aether-agents) | Published package; the badge resolves the live dist-tag. | | PyPI `aether-agent` | [![PyPI latest](https://img.shields.io/pypi/v/aether-agent?label=&color=3775a9)](https://pypi.org/project/aether-agent/) | Launcher that installs and runs the npm CLI; it fetches the npm `latest` dist-tag unless you pin one. | -| `main` source build | **0.3.2** | Current repository source: the 0.3 workflow plus every 0.3.1 maintenance fix. | +| `release/0.3` source | **0.3.3** | Maintenance source with the home-folder chat repair; check the live npm badge for the published package version. | The [release record](docs/releases/2026-08-22.md), [release notes](RELEASE_NOTES.md), and diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 65501a43..83b6c040 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,22 @@ +# Aether Agent v0.3.3 — home-folder chat recovery + +**September 27, 2026** + +- **Chat starts from broad home directories.** Nested `AGENTS.md` discovery now + has explicit entry, directory, and time limits. Hosted chat shows a warning + when discovery is incomplete and continues; local tool runs refuse until all + applicable local rules can be checked from a smaller project directory. +- **An HTTP error cannot hide behind a stalled detail body.** The CLI bounds + reading optional error details and keeps the known HTTP status, including + the sign-in hint for a 401. This does not assert that every reported 401 has + the same cause. +- **Stream errors remain failed turns.** The CLI shows known fixed public media + error text when `msg` is absent, or a clear fallback for untrusted `error` + text. A one-shot command exits nonzero even when a zero-cost `done` follows. +- **Public model documentation refreshed from Cloud's verified projection.** + The projection's digest is checked before generating the packed documentation. +--- + # Aether Agent v0.3.2 — one line again **September 3, 2026** diff --git a/docs/generated/model-catalogue.md b/docs/generated/model-catalogue.md index 1fd5c9cd..19dcdcec 100644 --- a/docs/generated/model-catalogue.md +++ b/docs/generated/model-catalogue.md @@ -1,68 +1,76 @@ - + # Public model catalogue snapshot Sanitized Cloud public model projection\. Listed availability describes publication status, not account entitlement\. -- Snapshot time: `2026-08-23T00:00:00.000Z` +- Snapshot time: `2026-09-27T21:15:38.529Z` - Source: Cloud public projection `model-catalogue-v1` -- Source digest: `sha256:80ba3ba1144d301e2cca407ceced74cb2b371f1da6e3982b87305ff12a3d4712` +- Source digest: `sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f` - Availability: `listed-not-entitled` - Offline fallback snapshot: yes -- Digest: `sha256:c9fe0efd73b5c390b0274e15640da7cbfe036e759d121984699d6d2d39564de1` +- Digest: `sha256:6062fe5946ece73f0f81ebf615629b9dab8bb14975ea0db110ee6002e469cc8a` | Model | ID | Provider | Modality | Tier | Availability | |---|---|---|---|---|---| -| AETHER Kronus V2\.4 | `aether-kronus-v2.4` | aether | text | pro | unavailable | +| AETHER Kronus V2\.4 | `aether-kronus-v2.4` | aether | text | pro | available | | AETHER Neo 5\.1T | `aether-neo-5.1t` | aether | text | solo | available | -| AETHER Vision | `aether-vision` | aether | multimodal | pro | unavailable | +| AETHER Vision | `aether-vision` | aether | multimodal | pro | available | | DeepSeek V4 Flash | `dsv4_flash` | text | text | free | available | | DeepSeek V4 Pro | `dsv4_pro` | text | text | pro | available | -| Gemini 3\.6 Flash | `gemini36_flash` | text | text | pro | unavailable | +| Claude Fable 5\.1 | `fable51` | text | text | pro | available | +| Gemini 3\.6 Flash | `gemini36_flash` | text | text | pro | available | +| Gemini 3\.8 Flash | `gemini38_flash` | text | text | pro | available | | Gemma 4 26B MoE | `gemma4_26b_moe` | text | text | pro | unavailable | -| Gemma 4 31B | `gemma4_31b` | text | text | pro | unavailable | +| Gemma 4 31B | `gemma4_31b` | text | text | pro | available | | Gemma 4 31B \(free\) | `gemma4_31b_free` | text | text | free | unavailable | | GPT\-5\.4 mini | `gpt54_mini` | text | text | solo | available | | GPT\-5\.5 | `gpt55` | text | text | pro | available | | GPT\-5\.6 Luna | `gpt56_luna` | text | text | pro | available | | GPT\-5\.6 Sol | `gpt56_sol` | text | text | pro | available | | GPT\-5\.6 Terra | `gpt56_terra` | text | text | pro | available | +| GPT\-6 Astra | `gpt6_astra` | text | text | pro | available | +| GPT\-6 Luna | `gpt6_luna` | text | text | pro | available | +| GPT\-6 Sol | `gpt6_sol` | text | text | pro | available | +| Grok 4\.6 | `grok46` | text | text | pro | available | | Claude Haiku 4\.5 | `haiku` | text | text | free | available | -| Kimi K2\.6 | `kimi_k26` | text | text | pro | unavailable | +| Kimi K2\.6 | `kimi_k26` | text | text | pro | available | | Kimi K2\.6 Thinking | `kimi_k26_thinking` | text | text | pro | unavailable | -| Kimi K3 | `kimi_k3` | text | text | pro | unavailable | +| Kimi K3 | `kimi_k3` | text | text | pro | available | | Claude Opus 4\.8 | `opus` | text | text | pro | available | | Claude Opus 5 | `opus5` | text | text | pro | available | +| Claude Opus 5\.5 | `opus55` | text | text | pro | available | +| Qwen 3\.8 Max | `qwen38_max` | text | text | pro | available | | Claude Sonnet 5 | `sonnet` | text | text | solo | available | -| FLUX\.2 Flex | `vision_flux2_flex` | image | image | solo | unavailable | -| FLUX\.2 Klein 4B | `vision_flux2_klein` | image | image | solo | unavailable | -| FLUX\.2 Max | `vision_flux2_max` | image | image | solo | unavailable | -| FLUX\.2 Pro | `vision_flux2_pro` | image | image | solo | unavailable | -| GPT Image 2 | `vision_gpt_image2` | image | image | solo | unavailable | -| GPT\-5 Image | `vision_gpt5_image` | image | image | solo | unavailable | -| GPT\-5 Image Mini | `vision_gpt5_image_mini` | image | image | solo | unavailable | -| Grok Imagine Video | `vision_grok_video` | video | video | pro | unavailable | -| Hailuo 2\.3 | `vision_hailuo23` | video | video | pro | unavailable | -| HappyHorse 1\.0 | `vision_happyhorse` | video | video | pro | unavailable | -| Kling 3\.0 Standard | `vision_kling` | video | video | pro | unavailable | -| Kling Video O1 | `vision_kling_o1` | video | video | pro | unavailable | -| Kling 3\.0 Pro | `vision_kling_pro` | video | video | pro | unavailable | -| Nano Banana | `vision_nano_banana` | image | image | solo | unavailable | -| Nano Banana 2 | `vision_nano_banana2` | image | image | solo | unavailable | -| Nano Banana Pro | `vision_nano_pro` | image | image | free | unavailable | -| Recraft V3 | `vision_recraft` | image | image | solo | unavailable | -| Recraft V4 | `vision_recraft_v4` | image | image | solo | unavailable | -| Riverflow V2 Fast | `vision_riverflow_fast` | image | image | solo | unavailable | -| Riverflow V2 Pro | `vision_riverflow_pro` | image | image | solo | unavailable | -| Seedance 2\.0 | `vision_seedance` | video | video | pro | unavailable | -| Seedance 2\.0 Fast | `vision_seedance_fast` | video | video | pro | unavailable | -| Seedance 1\.5 Pro | `vision_seedance15_pro` | video | video | pro | unavailable | -| Seedream 4\.5 | `vision_seedream45` | image | image | solo | unavailable | -| Sora 2 Pro | `vision_sora2_pro` | video | video | pro | unavailable | -| Veo 3\.1 | `vision_veo31` | video | video | pro | unavailable | -| Veo 3\.1 Fast | `vision_veo31_fast` | video | video | pro | unavailable | -| Veo 3\.1 Lite | `vision_veo31_lite` | video | video | pro | unavailable | -| Wan 2\.6 | `vision_wan26` | video | video | pro | unavailable | -| Wan 2\.7 | `vision_wan27` | video | video | pro | unavailable | +| FLUX\.2 Flex | `vision_flux2_flex` | image | image | solo | available | +| FLUX\.2 Klein 4B | `vision_flux2_klein` | image | image | solo | available | +| FLUX\.2 Max | `vision_flux2_max` | image | image | solo | available | +| FLUX\.2 Pro | `vision_flux2_pro` | image | image | solo | available | +| GPT Image 2 | `vision_gpt_image2` | image | image | solo | available | +| GPT\-5 Image | `vision_gpt5_image` | image | image | solo | available | +| GPT\-5 Image Mini | `vision_gpt5_image_mini` | image | image | solo | available | +| Grok Imagine Video | `vision_grok_video` | video | video | pro | available | +| Hailuo 2\.3 | `vision_hailuo23` | video | video | pro | available | +| HappyHorse 1\.0 | `vision_happyhorse` | video | video | pro | available | +| Kling 3\.0 Standard | `vision_kling` | video | video | pro | available | +| Kling Video O1 | `vision_kling_o1` | video | video | pro | available | +| Kling 3\.0 Pro | `vision_kling_pro` | video | video | pro | available | +| Nano Banana | `vision_nano_banana` | image | image | free | available | +| Nano Banana 2 | `vision_nano_banana2` | image | image | solo | available | +| Nano Banana Pro | `vision_nano_pro` | image | image | solo | available | +| Recraft V3 | `vision_recraft` | image | image | solo | available | +| Recraft V4 | `vision_recraft_v4` | image | image | solo | available | +| Riverflow V2 Fast | `vision_riverflow_fast` | image | image | solo | available | +| Riverflow V2 Pro | `vision_riverflow_pro` | image | image | solo | available | +| Seedance 2\.0 | `vision_seedance` | video | video | pro | available | +| Seedance 2\.0 Fast | `vision_seedance_fast` | video | video | pro | available | +| Seedance 1\.5 Pro | `vision_seedance15_pro` | video | video | pro | available | +| Seedream 4\.5 | `vision_seedream45` | image | image | solo | available | +| Sora 2 Pro | `vision_sora2_pro` | video | video | pro | available | +| Veo 3\.1 | `vision_veo31` | video | video | pro | available | +| Veo 3\.1 Fast | `vision_veo31_fast` | video | video | pro | available | +| Veo 3\.1 Lite | `vision_veo31_lite` | video | video | pro | available | +| Wan 2\.6 | `vision_wan26` | video | video | pro | available | +| Wan 2\.7 | `vision_wan27` | video | video | pro | available | Runtime availability is account-scoped. Use `aether models` while signed in for the authoritative live result. This snapshot contains no prices, spend caps, internal routes, or credentials. diff --git a/docs/model-catalogue/catalogue.json b/docs/model-catalogue/catalogue.json index e9607be5..4ccfff3c 100644 --- a/docs/model-catalogue/catalogue.json +++ b/docs/model-catalogue/catalogue.json @@ -1,10 +1,10 @@ { "schema": "aether-agent/public-model-catalogue@2", - "generatedAt": "2026-08-23T00:00:00.000Z", + "generatedAt": "2026-09-27T21:15:38.529Z", "source": { "kind": "cloud-public-projection", "version": "model-catalogue-v1", - "digest": "sha256:80ba3ba1144d301e2cca407ceced74cb2b371f1da6e3982b87305ff12a3d4712" + "digest": "sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f" }, "availabilitySemantics": "listed-not-entitled", "offlineFallback": true, @@ -17,7 +17,7 @@ "kind": "orchestrator", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "aether-neo-5.1t", @@ -35,7 +35,7 @@ "kind": "orchestrator", "tierMin": "pro", "modality": "multimodal", - "availability": "unavailable" + "availability": "available" }, { "id": "dsv4_flash", @@ -55,6 +55,15 @@ "modality": "text", "availability": "available" }, + { + "id": "fable51", + "label": "Claude Fable 5.1", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "gemini36_flash", "label": "Gemini 3.6 Flash", @@ -62,7 +71,16 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" + }, + { + "id": "gemini38_flash", + "label": "Gemini 3.8 Flash", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" }, { "id": "gemma4_26b_moe", @@ -80,7 +98,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "gemma4_31b_free", @@ -136,6 +154,42 @@ "modality": "text", "availability": "available" }, + { + "id": "gpt6_astra", + "label": "GPT-6 Astra", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "gpt6_luna", + "label": "GPT-6 Luna", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "gpt6_sol", + "label": "GPT-6 Sol", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "grok46", + "label": "Grok 4.6", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "haiku", "label": "Claude Haiku 4.5", @@ -152,7 +206,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "kimi_k26_thinking", @@ -170,7 +224,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "opus", @@ -190,6 +244,24 @@ "modality": "text", "availability": "available" }, + { + "id": "opus55", + "label": "Claude Opus 5.5", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "qwen38_max", + "label": "Qwen 3.8 Max", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "sonnet", "label": "Claude Sonnet 5", @@ -206,7 +278,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_klein", @@ -215,7 +287,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_max", @@ -224,7 +296,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_pro", @@ -233,7 +305,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt_image2", @@ -242,7 +314,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt5_image", @@ -251,7 +323,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt5_image_mini", @@ -260,7 +332,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_grok_video", @@ -269,7 +341,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_hailuo23", @@ -278,7 +350,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_happyhorse", @@ -287,7 +359,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling", @@ -296,7 +368,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling_o1", @@ -305,7 +377,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling_pro", @@ -314,16 +386,16 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_nano_banana", "label": "Nano Banana", "provider": "image", "kind": "model", - "tierMin": "solo", + "tierMin": "free", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_nano_banana2", @@ -332,16 +404,16 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_nano_pro", "label": "Nano Banana Pro", "provider": "image", "kind": "model", - "tierMin": "free", + "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_recraft", @@ -350,7 +422,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_recraft_v4", @@ -359,7 +431,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_riverflow_fast", @@ -368,7 +440,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_riverflow_pro", @@ -377,7 +449,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance", @@ -386,7 +458,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance_fast", @@ -395,7 +467,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance15_pro", @@ -404,7 +476,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedream45", @@ -413,7 +485,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_sora2_pro", @@ -422,7 +494,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31", @@ -431,7 +503,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31_fast", @@ -440,7 +512,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31_lite", @@ -449,7 +521,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_wan26", @@ -458,7 +530,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_wan27", @@ -467,8 +539,8 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" } ], - "digest": "sha256:c9fe0efd73b5c390b0274e15640da7cbfe036e759d121984699d6d2d39564de1" + "digest": "sha256:6062fe5946ece73f0f81ebf615629b9dab8bb14975ea0db110ee6002e469cc8a" } diff --git a/docs/model-catalogue/catalogue.source.json b/docs/model-catalogue/catalogue.source.json index a5669c44..ea3abd21 100644 --- a/docs/model-catalogue/catalogue.source.json +++ b/docs/model-catalogue/catalogue.source.json @@ -1,7 +1,7 @@ { "schema": "aether-cloud/public-model-projection@1", "sourceVersion": "model-catalogue-v1", - "generatedAt": "2026-08-23T00:00:00.000Z", + "generatedAt": "2026-09-27T21:15:38.529Z", "availabilitySemantics": "listed-not-entitled", "scopeNote": "Sanitized Cloud public model projection. Listed availability describes publication status, not account entitlement.", "models": [ @@ -33,13 +33,13 @@ "availability": "available" }, { - "id": "vision_nano_pro", - "label": "Nano Banana Pro", + "id": "vision_nano_banana", + "label": "Nano Banana", "provider": "image", "kind": "model", "tierMin": "free", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "dsv4_pro", @@ -50,6 +50,15 @@ "modality": "text", "availability": "available" }, + { + "id": "fable51", + "label": "Claude Fable 5.1", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "gemini36_flash", "label": "Gemini 3.6 Flash", @@ -57,7 +66,16 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" + }, + { + "id": "gemini38_flash", + "label": "Gemini 3.8 Flash", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" }, { "id": "gemma4_26b_moe", @@ -75,7 +93,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "gpt55", @@ -113,6 +131,42 @@ "modality": "text", "availability": "available" }, + { + "id": "gpt6_astra", + "label": "GPT-6 Astra", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "gpt6_luna", + "label": "GPT-6 Luna", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "gpt6_sol", + "label": "GPT-6 Sol", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "grok46", + "label": "Grok 4.6", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "kimi_k26", "label": "Kimi K2.6", @@ -120,7 +174,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "kimi_k26_thinking", @@ -138,7 +192,7 @@ "kind": "model", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "opus", @@ -158,6 +212,24 @@ "modality": "text", "availability": "available" }, + { + "id": "opus55", + "label": "Claude Opus 5.5", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, + { + "id": "qwen38_max", + "label": "Qwen 3.8 Max", + "provider": "text", + "kind": "model", + "tierMin": "pro", + "modality": "text", + "availability": "available" + }, { "id": "vision_grok_video", "label": "Grok Imagine Video", @@ -165,7 +237,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_hailuo23", @@ -174,7 +246,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_happyhorse", @@ -183,7 +255,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling", @@ -192,7 +264,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling_o1", @@ -201,7 +273,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_kling_pro", @@ -210,7 +282,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance", @@ -219,7 +291,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance15_pro", @@ -228,7 +300,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedance_fast", @@ -237,7 +309,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_sora2_pro", @@ -246,7 +318,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31", @@ -255,7 +327,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31_fast", @@ -264,7 +336,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_veo31_lite", @@ -273,7 +345,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_wan26", @@ -282,7 +354,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_wan27", @@ -291,7 +363,7 @@ "kind": "model", "tierMin": "pro", "modality": "video", - "availability": "unavailable" + "availability": "available" }, { "id": "gpt54_mini", @@ -318,7 +390,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_klein", @@ -327,7 +399,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_max", @@ -336,7 +408,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_flux2_pro", @@ -345,7 +417,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt5_image", @@ -354,7 +426,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt5_image_mini", @@ -363,7 +435,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_gpt_image2", @@ -372,25 +444,25 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { - "id": "vision_nano_banana", - "label": "Nano Banana", + "id": "vision_nano_banana2", + "label": "Nano Banana 2", "provider": "image", "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { - "id": "vision_nano_banana2", - "label": "Nano Banana 2", + "id": "vision_nano_pro", + "label": "Nano Banana Pro", "provider": "image", "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_recraft", @@ -399,7 +471,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_recraft_v4", @@ -408,7 +480,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_riverflow_fast", @@ -417,7 +489,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_riverflow_pro", @@ -426,7 +498,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "vision_seedream45", @@ -435,7 +507,7 @@ "kind": "model", "tierMin": "solo", "modality": "image", - "availability": "unavailable" + "availability": "available" }, { "id": "aether-kronus-v2.4", @@ -444,7 +516,7 @@ "kind": "orchestrator", "tierMin": "pro", "modality": "text", - "availability": "unavailable" + "availability": "available" }, { "id": "aether-vision", @@ -453,7 +525,7 @@ "kind": "orchestrator", "tierMin": "pro", "modality": "multimodal", - "availability": "unavailable" + "availability": "available" }, { "id": "aether-neo-5.1t", @@ -465,5 +537,5 @@ "availability": "available" } ], - "digest": "sha256:80ba3ba1144d301e2cca407ceced74cb2b371f1da6e3982b87305ff12a3d4712" + "digest": "sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f" } diff --git a/docs/model-catalogue/index.html b/docs/model-catalogue/index.html index 68f00d6d..965bf79f 100644 --- a/docs/model-catalogue/index.html +++ b/docs/model-catalogue/index.html @@ -3,21 +3,21 @@ Aether Agent public model catalogue

Public model catalogue snapshot

Sanitized Cloud public model projection. Listed availability describes publication status, not account entitlement.

-

Snapshot: · Cloud source: model-catalogue-v1 · Source digest: sha256:80ba3ba1144d301e2cca407ceced74cb2b371f1da6e3982b87305ff12a3d4712 · Catalogue digest: sha256:c9fe0efd73b5c390b0274e15640da7cbfe036e759d121984699d6d2d39564de1 · Offline fallback: yes

+

Snapshot: · Cloud source: model-catalogue-v1 · Source digest: sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f · Catalogue digest: sha256:6062fe5946ece73f0f81ebf615629b9dab8bb14975ea0db110ee6002e469cc8a · Offline fallback: yes

This page is useful without JavaScript. Runtime availability is account-scoped; use aether models while signed in for the authoritative live result. No prices or spend caps are asserted here.

-
+

AETHER Kronus V2.4

aether-kronus-v2.4

-
Provider
aether
Modality
text
Minimum documented tier
pro
Availability
unavailable
+
Provider
aether
Modality
text
Minimum documented tier
pro
Availability
available

AETHER Neo 5.1T

aether-neo-5.1t

Provider
aether
Modality
text
Minimum documented tier
solo
Availability
available
-
+

AETHER Vision

aether-vision

-
Provider
aether
Modality
multimodal
Minimum documented tier
pro
Availability
unavailable
+
Provider
aether
Modality
multimodal
Minimum documented tier
pro
Availability
available

DeepSeek V4 Flash

dsv4_flash

@@ -27,17 +27,25 @@

DeepSeek V4 Flash

dsv4_flash

DeepSeek V4 Pro

dsv4_pro

Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
-
+
+

Claude Fable 5.1

fable51

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+

Gemini 3.6 Flash

gemini36_flash

-
Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+
+

Gemini 3.8 Flash

gemini38_flash

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available

Gemma 4 26B MoE

gemma4_26b_moe

Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
-
+

Gemma 4 31B

gemma4_31b

-
Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available

Gemma 4 31B (free)

gemma4_31b_free

@@ -63,21 +71,37 @@

GPT-5.6 Sol

gpt56_sol

GPT-5.6 Terra

gpt56_terra

Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+

GPT-6 Astra

gpt6_astra

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+
+

GPT-6 Luna

gpt6_luna

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+
+

GPT-6 Sol

gpt6_sol

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+
+

Grok 4.6

grok46

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+

Claude Haiku 4.5

haiku

Provider
text
Modality
text
Minimum documented tier
free
Availability
available
-
+

Kimi K2.6

kimi_k26

-
Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available

Kimi K2.6 Thinking

kimi_k26_thinking

Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
-
+

Kimi K3

kimi_k3

-
Provider
text
Modality
text
Minimum documented tier
pro
Availability
unavailable
+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available

Claude Opus 4.8

opus

@@ -87,129 +111,137 @@

Claude Opus 4.8

opus

Claude Opus 5

opus5

Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+

Claude Opus 5.5

opus55

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+
+
+

Qwen 3.8 Max

qwen38_max

+
Provider
text
Modality
text
Minimum documented tier
pro
Availability
available
+

Claude Sonnet 5

sonnet

Provider
text
Modality
text
Minimum documented tier
solo
Availability
available
-
+

FLUX.2 Flex

vision_flux2_flex

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

FLUX.2 Klein 4B

vision_flux2_klein

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

FLUX.2 Max

vision_flux2_max

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

FLUX.2 Pro

vision_flux2_pro

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

GPT Image 2

vision_gpt_image2

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

GPT-5 Image

vision_gpt5_image

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

GPT-5 Image Mini

vision_gpt5_image_mini

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Grok Imagine Video

vision_grok_video

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Hailuo 2.3

vision_hailuo23

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

HappyHorse 1.0

vision_happyhorse

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Kling 3.0 Standard

vision_kling

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Kling Video O1

vision_kling_o1

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Kling 3.0 Pro

vision_kling_pro

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Nano Banana

vision_nano_banana

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
free
Availability
available
-
+

Nano Banana 2

vision_nano_banana2

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Nano Banana Pro

vision_nano_pro

-
Provider
image
Modality
image
Minimum documented tier
free
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Recraft V3

vision_recraft

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Recraft V4

vision_recraft_v4

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Riverflow V2 Fast

vision_riverflow_fast

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Riverflow V2 Pro

vision_riverflow_pro

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Seedance 2.0

vision_seedance

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Seedance 2.0 Fast

vision_seedance_fast

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Seedance 1.5 Pro

vision_seedance15_pro

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Seedream 4.5

vision_seedream45

-
Provider
image
Modality
image
Minimum documented tier
solo
Availability
unavailable
+
Provider
image
Modality
image
Minimum documented tier
solo
Availability
available
-
+

Sora 2 Pro

vision_sora2_pro

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Veo 3.1

vision_veo31

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Veo 3.1 Fast

vision_veo31_fast

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Veo 3.1 Lite

vision_veo31_lite

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Wan 2.6

vision_wan26

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
-
+

Wan 2.7

vision_wan27

-
Provider
video
Modality
video
Minimum documented tier
pro
Availability
unavailable
+
Provider
video
Modality
video
Minimum documented tier
pro
Availability
available
diff --git a/docs/releases/2026-09-27.md b/docs/releases/2026-09-27.md new file mode 100644 index 00000000..6d56c408 --- /dev/null +++ b/docs/releases/2026-09-27.md @@ -0,0 +1,11 @@ +# Aether Agent v0.3.3 — home-folder chat recovery candidate + +**September 27, 2026.** This maintenance candidate starts hosted chat after a +bounded nested-instruction scan in broad home directories, shows an explicit +warning if local rules were not fully discovered, and keeps local tool runs +closed in that case. A stalled HTTP error-detail body now yields its known +status within a short bound. The public model catalogue is refreshed from the +verified Cloud projection; source tests and packed-package checks are required +before any release decision. + +Packet: [OPERATOR-PACKET-v0.3.3.md](OPERATOR-PACKET-v0.3.3.md). diff --git a/docs/releases/OPERATOR-PACKET-v0.3.3.md b/docs/releases/OPERATOR-PACKET-v0.3.3.md new file mode 100644 index 00000000..7f6d94f2 --- /dev/null +++ b/docs/releases/OPERATOR-PACKET-v0.3.3.md @@ -0,0 +1,51 @@ +# Operator packet — Aether Agent v0.3.3 candidate + +This packet describes a proposed maintenance patch over the already published +v0.3.2 package. It is a source candidate, not a release authorization. + +| | | +|---|---| +| Package | `aether-agents` | +| Proposed tag | `v0.3.3` | +| Release line base | `e234bf6bcd283dd81691158e70e826a47013eea6` (`release/0.3`, synced to published v0.3.2) | +| Candidate branch | `fix/032-home-scan` | +| Release scope | Bounded nested `AGENTS.md` discovery on hosted chat; incomplete discovery visibly warns, while local tool runs refuse. Finite reads of non-2xx HTTP detail bodies preserve the known status. Cloud stream errors render only known fixed public media text from the alternate `error` field, otherwise a fallback, and fail one-shot commands. The generated public model catalogue is refreshed from Cloud's verified projection. | +| Catalogue source evidence | HTTP 200 from `https://api.aethersystems.net/cloud/public/model-catalogue` on September 27, 2026; schema `aether-cloud/public-model-projection@1`, generatedAt `2026-09-27T21:15:38.529Z`, 59 safe rows, canonical digest `sha256:f4601f14ac7829b51e95d92abc1631c74097fa193bc566151a2161c5a799ba7f`. The checked-in raw projection passed the generator's digest and freshness validation before regeneration. | +| Archive evidence | No v0.3.3 release archive or published npm package exists. Exact-head hosted package evidence is pending. | +| Package manifest | The local packed REPL smoke installs the candidate tarball; hosted Windows and Ubuntu evidence is required on the final patch head. | +| Provenance evidence | Pending a future trusted-publishing workflow; no v0.3.3 provenance attestation is claimed. | +| PyPI launcher | The launcher source version is synced to 0.3.3; no PyPI publication is claimed. | +| Required qualification | Full exact-head Windows and Ubuntu tests, supply-chain and release-truth checks, installed-tarball home-folder smoke, and a real Windows/account canary before publication. | +| Rollback | If a later publication regresses, restore npm `latest` to v0.3.2; do not unpublish an existing version. | + +## Limits + +The nested scan uses synchronous filesystem reads. A single unusually slow +directory read can exceed the wall-clock target; the budget bounds work between +reads and reports incomplete discovery when observed. The packed smoke uses a +synthetic home directory and local HTTP server. It does not prove the user's +account or hosted service response, nor diagnose every HTTP 401. +Cloud errors still need server-side diagnosis. The alternate `error` field has +historically carried raw exception text, so the client shows only known fixed +public media strings from that field; all other values get a truthful fallback. +The client cannot resolve the server's underlying failure. + +## Commands retained without a new release-note invocation + +These existing visible commands are retained by this maintenance patch: + +- `aether help` +- `aether chat` +- `aether run` +- `aether agents` +- `aether github` +- `aether vault` +- `aether workflow` +- `aether memory` +- `aether image` +- `aether video` +- `aether output` +- `aether audit` +- `aether receipt` +- `aether mcp` +- `aether config` diff --git a/docs/releases/README.md b/docs/releases/README.md index 95417568..478141bd 100644 --- a/docs/releases/README.md +++ b/docs/releases/README.md @@ -12,6 +12,10 @@ manifest, the evidence gathered, and the founder-owned steps that publish it. ## Index +- [2026-09-27](2026-09-27.md) — **v0.3.3** maintenance candidate: bounded + home-folder instruction discovery, finite HTTP error-body reads, and a + refreshed verified public model catalogue. Packet: + [OPERATOR-PACKET-v0.3.3.md](OPERATOR-PACKET-v0.3.3.md). - [2026-09-03](2026-09-03.md) — **v0.3.2** patch candidate: the v0.3.1 maintenance line merged back into `main`, restoring the browser-launcher verification and putting the declared version ahead of the published one. diff --git a/package-lock.json b/package-lock.json index da90f5b0..05d3ca96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aether-agents", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aether-agents", - "version": "0.3.2", + "version": "0.3.3", "license": "Apache-2.0", "bin": { "aether": "dist/src/main.js", diff --git a/package.json b/package.json index 42112342..02416b47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aether-agents", - "version": "0.3.2", + "version": "0.3.3", "description": "Open-source terminal coding agent for hosted models or local Ollama, with repository-aware tools, operator-controlled execution, and verified results.", "type": "module", "bin": { @@ -39,6 +39,7 @@ "start": "node dist/src/main.js", "test": "npm run build && node --test --test-isolation=none \"dist/test/**/*.test.js\"", "smoke": "npm run build && node -e \"import('./dist/src/core/smoke.js').then(m=>m.smokeMain()).then(c=>process.exit(c)).catch(e=>{console.error(e);process.exit(1)})\"", + "smoke:packed-home": "npm run build && node dist/scripts/packed-home-chat-smoke.js", "verify:production": "npm run build && node dist/scripts/verify-production.js", "release:truth": "npm run build && node dist/scripts/release-truth.js", "docs:generate": "npm run build && node dist/scripts/generate-docs.js", diff --git a/packages/pypi-cli/pyproject.toml b/packages/pypi-cli/pyproject.toml index 0254e378..14db1ccf 100644 --- a/packages/pypi-cli/pyproject.toml +++ b/packages/pypi-cli/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "hatchling.build" # The pip/pipx front door to the Aether Agent CLI. Aether Agent itself is the Node package # `aether-agents` on npm; this package installs and launches it. The version below tracks -# main's package.json so the two halves of the repository release together -- it is not the +# this branch's package.json so the two halves of the repository stay aligned -- it is not the # version installed, which is the npm `latest` dist-tag unless pinned. # packages/sync-version.mjs copies package.json's version here, and tests/test_packaging.py # fails the build if they drift. [project] name = "aether-agent" -version = "0.3.2" +version = "0.3.3" description = "Install and run Aether Agent, the open-source terminal coding agent, from pip or pipx." readme = "README.md" requires-python = ">=3.10" diff --git a/packages/pypi-cli/src/aether_agent/__init__.py b/packages/pypi-cli/src/aether_agent/__init__.py index 2b8dc098..0ccba790 100644 --- a/packages/pypi-cli/src/aether_agent/__init__.py +++ b/packages/pypi-cli/src/aether_agent/__init__.py @@ -5,15 +5,15 @@ CLI without hand-rolling an npm global install, and every command you type is forwarded to that CLI unchanged. -The version here tracks main's npm package version, so the two halves of the repository -release together. It is not the version installed: the launcher fetches the npm `latest` +The version here tracks this branch's npm package version, so the two halves of the +repository stay aligned. It is not the version installed: the launcher fetches the npm `latest` dist-tag unless you pin one, so `pipx install aether-agent` and `npm install -g aether-agents@latest` land on the same agent. """ from __future__ import annotations -__version__ = "0.3.2" +__version__ = "0.3.3" #: The npm package this launcher installs and runs. NPM_PACKAGE = "aether-agents" diff --git a/scripts/packed-home-chat-smoke.ts b/scripts/packed-home-chat-smoke.ts new file mode 100644 index 00000000..378ebba5 --- /dev/null +++ b/scripts/packed-home-chat-smoke.ts @@ -0,0 +1,184 @@ +// Exercise the installed npm tarball from a synthetic, broad home directory. +// The fixture crosses the instruction-scan entry budget, so the warning is +// observable and a 0.3.2 package fails this smoke even if its full scan happens +// to finish quickly on a fast CI runner. No real home files or credentials are used. +import { spawn, spawnSync } from "node:child_process"; +import { createServer } from "node:http"; +import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { basename, dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { AddressInfo } from "node:net"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const prompt = "just testing reply 1"; +const continuationPrompt = "just testing reply 2"; + +function npm(args: string[], cwd: string): string { + const npmCli = process.platform === "win32" + ? [process.env["npm_execpath"], join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")] + .find((path) => path && existsSync(path)) + : undefined; + if (process.platform === "win32" && !npmCli) throw new Error("Node's npm CLI was not found beside the installed Node runtime"); + const result = spawnSync(npmCli ? process.execPath : "npm", npmCli ? [npmCli, ...args] : args, { + cwd, + encoding: "utf8", + timeout: 120_000, + }); + if (result.error || result.status !== 0) { + throw new Error(`npm ${args[0]} failed: ${result.error?.message ?? result.stderr?.slice(-2_000) ?? result.status}`); + } + return result.stdout; +} + +async function runInstalled(main: string, cwd: string, baseUrl: string, configDir: string, lines: string, streamTimeoutMs?: number, oneShotPrompt?: string): Promise<{ stdout: string; stderr: string; code: number | null; timedOut: boolean }> { + const child = spawn(process.execPath, oneShotPrompt ? [main, "chat", oneShotPrompt] : [main], { + cwd, + env: { + ...process.env, + HOME: cwd, + USERPROFILE: cwd, + AETHER_CONFIG_DIR: configDir, + AETHER_TOKEN: "aek_packed_smoke_only", + AETHER_BACKEND: "cloud", + AETHER_BASE_URL: baseUrl, + AETHER_NO_HISTORY: "1", + ...(streamTimeoutMs ? { AETHER_STREAM_TIMEOUT_MS: String(streamTimeoutMs) } : {}), + NO_COLOR: "1", + }, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + child.stdout.setEncoding("utf8").on("data", (chunk: string) => { stdout += chunk.slice(0, 65_536); }); + child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr += chunk.slice(0, 65_536); }); + const timer = setTimeout(() => { timedOut = true; child.kill(); }, 20_000); + child.stdin.end(lines); + try { + const code = await new Promise((resolveExit, reject) => { + child.once("error", reject); + child.once("close", resolveExit); + }); + return { stdout, stderr, code, timedOut }; + } finally { + clearTimeout(timer); + } +} + +async function main(): Promise { + const fixture = mkdtempSync(join(tmpdir(), "aether-packed-home-")); + const home = join(fixture, "home"); + const prefix = join(fixture, "prefix"); + const configDir = join(fixture, "config"); + mkdirSync(home); + // A single real home directory can contain thousands of child names. This + // deterministic fixture trips the same entry cap without touching user data. + for (let i = 0; i < 2_100; i++) mkdirSync(join(home, `folder-${String(i).padStart(4, "0")}`)); + let requests = 0; + let requestBodies: string[] = []; + let mode: "success" | "recover-401" | "empty-error" | "error-field" | "private-error" = "success"; + let heldResponse: import("node:http").ServerResponse | undefined; + const server = createServer((request, response) => { + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk: string) => { body += chunk.slice(0, 32_768); }); + request.on("end", () => { + if (request.url?.endsWith("/agent/chat/stream")) { + requests++; + requestBodies.push(body); + if (mode === "recover-401" && requests === 1) { + // Headers arrive, but the optional JSON detail never completes. + // The installed CLI must show the known 401, then accept the next + // queued REPL prompt without retrying the uncertain first request. + response.writeHead(401, { "Content-Type": "application/json" }); + response.write('{"detail":'); + heldResponse = response; + return; + } + if (mode === "empty-error" || mode === "error-field" || mode === "private-error") { + response.writeHead(200, { "Content-Type": "text/event-stream" }); + const error = mode === "error-field" + ? { type: "error", error: "The media studio agent hit an error.", reason: "private internal reason" } + : mode === "private-error" + ? { type: "error", error: "private provider failure: internal stack trace", reason: "private internal reason" } + : { type: "error", msg: "" }; + response.end(`data: ${JSON.stringify(error)}\n\ndata: {"type":"done","uvt":0,"cents":0}\n\n`); + return; + } + response.writeHead(200, { "Content-Type": "text/event-stream" }); + const answer = body.includes(continuationPrompt) ? "2" : "1"; + response.end(`data: {"type":"delta","text":"${answer}"}\n\ndata: {"type":"done","uvt":1,"cents":0}\n\n`); + } else if (request.url?.endsWith("/models")) { + response.writeHead(200, { "Content-Type": "application/json" }); + response.end('{"models":[]}'); + } else { + response.writeHead(404, { "Content-Type": "application/json" }); + response.end('{"detail":"unexpected smoke route"}'); + } + }); + }); + try { + const givenTarball = process.argv[2]; + const tarball = givenTarball + ? resolve(repoRoot, givenTarball) + : join(fixture, (JSON.parse(npm(["pack", "--json", "--ignore-scripts", "--pack-destination", fixture], repoRoot)) as Array<{ filename: string }>)[0]!.filename); + if (!existsSync(tarball)) throw new Error("packed npm tarball is missing"); + npm(["install", "--global", "--prefix", prefix, tarball, "--ignore-scripts", "--no-audit", "--no-fund"], repoRoot); + const installedRoot = join(prefix, process.platform === "win32" ? "node_modules" : "lib/node_modules", "aether-agents"); + const installedMain = join(installedRoot, "dist", "src", "main.js"); + if (!existsSync(installedMain)) throw new Error("installed package has no CLI entrypoint"); + + await new Promise((resolveListen, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolveListen); + }); + const port = (server.address() as AddressInfo).port; + const baseUrl = `http://127.0.0.1:${port}/cloud`; + const result = await runInstalled(installedMain, home, baseUrl, configDir, prompt + "\n"); + if (result.timedOut || result.code !== 0 || requests !== 1 || !requestBodies[0]?.includes(prompt) || !/\b1\b/.test(result.stdout) || !/nested AGENTS\.md scan reached 2048 entries/.test(result.stderr)) { + throw new Error(`packed home chat smoke failed: ${JSON.stringify({ ...result, requests, requestBodies })}`); + } + mode = "recover-401"; + requests = 0; + requestBodies = []; + const recovered = await runInstalled(installedMain, home, baseUrl, configDir, `${prompt}\n${continuationPrompt}\n`, 800); + if (recovered.timedOut || recovered.code !== 0 || requests !== 2 || !requestBodies[0]?.includes(prompt) || !requestBodies[1]?.includes(continuationPrompt) || !/HTTP 401/.test(recovered.stderr) || !/aether auth login/.test(recovered.stderr) || !/\b2\b/.test(recovered.stdout)) { + throw new Error(`packed 401 continuation smoke failed: ${JSON.stringify({ ...recovered, requests, requestBodies })}`); + } + heldResponse?.destroy(); + mode = "empty-error"; + requests = 0; + requestBodies = []; + const emptyError = await runInstalled(installedMain, home, baseUrl, configDir, "", undefined, prompt); + if (emptyError.timedOut || emptyError.code !== 1 || requests !== 1 || !requestBodies[0]?.includes(prompt) || !/cloud turn failed; the server did not provide details/.test(emptyError.stderr) || /\n✗\s*\n/.test(emptyError.stderr)) { + throw new Error(`packed empty-error one-shot smoke failed: ${JSON.stringify({ ...emptyError, requests, requestBodies })}`); + } + mode = "error-field"; + requests = 0; + requestBodies = []; + const publicError = await runInstalled(installedMain, home, baseUrl, configDir, "", undefined, prompt); + if (publicError.timedOut || publicError.code !== 1 || requests !== 1 || !requestBodies[0]?.includes(prompt) || !/The media studio agent hit an error\./.test(publicError.stderr) || /private internal reason/.test(publicError.stderr)) { + throw new Error(`packed public-error one-shot smoke failed: ${JSON.stringify({ ...publicError, requests, requestBodies })}`); + } + mode = "private-error"; + requests = 0; + requestBodies = []; + const privateError = await runInstalled(installedMain, home, baseUrl, configDir, "", undefined, prompt); + if (privateError.timedOut || privateError.code !== 1 || requests !== 1 || !requestBodies[0]?.includes(prompt) || !/cloud turn failed; the server did not provide details/.test(privateError.stderr) || /private provider failure|internal stack trace|private internal reason/.test(privateError.stderr)) { + throw new Error(`packed private-error one-shot smoke failed: ${JSON.stringify({ ...privateError, requests, requestBodies })}`); + } + process.stdout.write("packed home chat smoke passed: bounded scan, answer 1, visible 401 and continued answer 2, safe error fallback exits 1\n"); + } finally { + heldResponse?.destroy(); + await new Promise((resolveClose) => server.close(() => resolveClose())); + if (dirname(fixture) === tmpdir() && basename(fixture).startsWith("aether-packed-home-")) { + rmSync(fixture, { recursive: true, force: true }); + } + } +} + +main().catch((error: unknown) => { + process.stderr.write((error instanceof Error ? error.message : String(error)) + "\n"); + process.exitCode = 1; +}); diff --git a/scripts/release-truth.ts b/scripts/release-truth.ts index 36e72d4f..7ad3218c 100644 --- a/scripts/release-truth.ts +++ b/scripts/release-truth.ts @@ -433,7 +433,7 @@ const PINNED_REGISTRY_CLAIM_PATTERNS: readonly { id: string; pattern: RegExp }[] export function derivePublicRegistryClaim(docs: Record): PublicRegistryClaim { const readme = docs["README.md"] ?? ""; - const sourceVersion = /\|\s*`main`\s+source build\s*\|\s*\*\*([^*]+)\*\*/i.exec(readme)?.[1]?.trim() ?? null; + const sourceVersion = /\|\s*`(?:main|release\/0\.3)`\s+source(?: build)?\s*\|\s*\*\*([^*]+)\*\*/i.exec(readme)?.[1]?.trim() ?? null; const pinnedRegistryClaims = Object.entries(docs) .flatMap(([path, text]) => PINNED_REGISTRY_CLAIM_PATTERNS.filter((candidate) => candidate.pattern.test(text)).map((candidate) => `${path}: ${candidate.id}`)) .sort(); diff --git a/src/commands/chat.ts b/src/commands/chat.ts index 7df900a1..ed4b16b7 100644 --- a/src/commands/chat.ts +++ b/src/commands/chat.ts @@ -124,6 +124,7 @@ export async function runTurn( const opened = openRunSession({ projectRoot: ctx.flags.cwd, prompt, + allowIncompleteInstructionDiscovery: backend === "cloud", ...(skillOpts.explicitSkill ? { explicitSkill: skillOpts.explicitSkill } : {}), ...(skillOpts.noSkills ? { noSkills: true } : {}), }); @@ -263,7 +264,10 @@ async function runCloudTurn( onFrame?.(frame); renderer.frame(frame); } - if (sawError) throw new ChatTurnError(sawError); + // Presence of an error frame is authoritative even when its message is + // empty. A later done frame can carry cost, but cannot turn failure into + // success (or let a one-shot command exit zero). + if (sawError !== null) throw new ChatTurnError(sawError); if (!sawTerminal) throw new StreamIncompleteError(); } catch (err) { if (err instanceof StreamUnavailableError) { diff --git a/src/core/instructions/instruction_discovery.ts b/src/core/instructions/instruction_discovery.ts index ef36a74d..baf60323 100644 --- a/src/core/instructions/instruction_discovery.ts +++ b/src/core/instructions/instruction_discovery.ts @@ -5,8 +5,10 @@ // fetches includes, and caps file size and source count honestly. import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs"; +import type { Dirent } from "node:fs"; import { join, relative, resolve, sep } from "node:path"; import { createHash } from "node:crypto"; +import { performance } from "node:perf_hooks"; import { configDir } from "../config.js"; import { SKILL_BOUNDS } from "../skills/skill_bounds.js"; import type { InstructionSource, InstructionSourceKind } from "./instruction_types.js"; @@ -87,21 +89,49 @@ export function parseCursorGlobs(content: string): { globs: readonly string[] | return { globs: globs.length ? globs : null, warnings, body }; } -/** Locate nested AGENTS.md files, bounded by depth; skips dot/vendor dirs. */ -function findNestedAgents(projectRoot: string): string[] { +/** Locate nested AGENTS.md files with explicit work and wall-clock limits. */ +function findNestedAgents(projectRoot: string): { paths: string[]; complete: boolean; reason: string } { const found: string[] = []; const skip = new Set(["node_modules", "dist", "build", "vendor", "target", ".git"]); + const started = performance.now(); + let directories = 0; + let entriesSeen = 0; + let reason = ""; + const overBudget = (): boolean => { + if (reason) return true; + if (performance.now() - started >= SKILL_BOUNDS.maxNestedInstructionScanMs) { + reason = `nested AGENTS.md scan exceeded ${SKILL_BOUNDS.maxNestedInstructionScanMs} ms`; + return true; + } + if (directories >= SKILL_BOUNDS.maxNestedInstructionDirectories) { + reason = `nested AGENTS.md scan reached ${SKILL_BOUNDS.maxNestedInstructionDirectories} directories`; + return true; + } + return false; + }; const walk = (dir: string, depth: number): void => { if (depth > SKILL_BOUNDS.maxNestedInstructionDepth) return; - let entries: string[]; + if (overBudget()) return; + directories++; + let entries: Dirent[]; try { - entries = readdirSync(dir); + entries = readdirSync(dir, { withFileTypes: true }); } catch { return; } - for (const entry of entries.sort()) { - if (entry.startsWith(".") || skip.has(entry)) continue; - const full = join(dir, entry); + // A single directory can contain tens of thousands of names. Do not sort + // or stat any of them when the remaining entry budget cannot cover it. + // The readdirSync call itself is one synchronous filesystem operation; the + // incomplete marker below makes this limit explicit to callers. + if (entriesSeen + entries.length > SKILL_BOUNDS.maxNestedInstructionEntries) { + reason = `nested AGENTS.md scan reached ${SKILL_BOUNDS.maxNestedInstructionEntries} entries`; + return; + } + for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) { + if (overBudget()) return; + entriesSeen++; + if (entry.name.startsWith(".") || skip.has(entry.name) || !entry.isDirectory()) continue; + const full = join(dir, entry.name); let isDirectory = false; try { isDirectory = lstatSync(full).isDirectory(); @@ -116,12 +146,13 @@ function findNestedAgents(projectRoot: string): string[] { } }; walk(projectRoot, 1); - return found; + return { paths: found, complete: !reason, reason }; } export function discoverInstructionSources(projectRoot: string): { sources: InstructionSource[]; skipped: { path: string; reason: string }[]; + nestedScanComplete: boolean; } { const root = resolve(projectRoot); const candidates: DiscoveredFile[] = []; @@ -132,7 +163,8 @@ export function discoverInstructionSources(projectRoot: string): { addIfPresent("aether-project", join(root, ".aether", "instructions.md")); addIfPresent("agents-root", join(root, "AGENTS.md")); - for (const nested of findNestedAgents(root)) { + const nestedScan = findNestedAgents(root); + for (const nested of nestedScan.paths) { const scopeDir = relative(root, join(nested, "..")).split(sep).join("/"); candidates.push({ kind: "agents-nested", path: nested, scopeDir, globs: null, warnings: [] }); } @@ -156,6 +188,12 @@ export function discoverInstructionSources(projectRoot: string): { const sources: InstructionSource[] = []; const skipped: { path: string; reason: string }[] = []; + if (!nestedScan.complete) { + skipped.push({ + path: "**/AGENTS.md", + reason: nestedScan.reason + "; nested project rules may be missing — start in a smaller project directory", + }); + } for (const candidate of candidates) { if (sources.length >= SKILL_BOUNDS.maxInstructionSources) { @@ -201,5 +239,5 @@ export function discoverInstructionSources(projectRoot: string): { }); } - return { sources, skipped }; + return { sources, skipped, nestedScanComplete: nestedScan.complete }; } diff --git a/src/core/instructions/instruction_resolver.ts b/src/core/instructions/instruction_resolver.ts index 14a5beed..d0c70ac2 100644 --- a/src/core/instructions/instruction_resolver.ts +++ b/src/core/instructions/instruction_resolver.ts @@ -150,7 +150,7 @@ export function runScopedSources(sources: readonly InstructionSource[]): Instruc /** Build the full graph for a project: discovery + run-scope conflict pass. */ export function resolveInstructionGraph(projectRoot: string): InstructionGraph { - const { sources, skipped } = discoverInstructionSources(projectRoot); + const { sources, skipped, nestedScanComplete } = discoverInstructionSources(projectRoot); // runScopedSources drops a source whose glob frontmatter would not parse: its // scope is unknown, so applying it to the whole run would be a guess about // which files it governs. Dropping it is right; dropping it SILENTLY is not — @@ -165,6 +165,7 @@ export function resolveInstructionGraph(projectRoot: string): InstructionGraph { sources, conflicts: detectConflicts(runScopedSources(sources)), skipped: [...skipped, ...unparsable], + nestedScanComplete, }; } diff --git a/src/core/instructions/instruction_types.ts b/src/core/instructions/instruction_types.ts index e5d182b1..6340d154 100644 --- a/src/core/instructions/instruction_types.ts +++ b/src/core/instructions/instruction_types.ts @@ -66,4 +66,6 @@ export interface InstructionGraph { conflicts: readonly InstructionConflict[]; /** Sources discovered but skipped, with a visible reason (over cap, bad encoding). */ skipped: readonly { path: string; reason: string }[]; + /** False when nested AGENTS.md discovery stopped at a scan limit. */ + nestedScanComplete: boolean; } diff --git a/src/core/skills/run_session.ts b/src/core/skills/run_session.ts index abd32654..7cd2b799 100644 --- a/src/core/skills/run_session.ts +++ b/src/core/skills/run_session.ts @@ -51,6 +51,8 @@ export interface RunSessionOptions { noSkills?: boolean; /** Injected for tests. */ builtinRoot?: string; + /** Hosted cloud chat has no local tool authority; it may proceed with a visible scan warning. */ + allowIncompleteInstructionDiscovery?: boolean; /** * The operator's live permissions. Skills never contribute to this set — it * is passed in so a caller can narrow it, and so no cached policy can widen @@ -392,6 +394,12 @@ export function openRunSession(options: RunSessionOptions): OpenRunSession { ...(options.noSkills ? { noSkills: true } : {}), ...(options.builtinRoot ? { builtinRoot: options.builtinRoot } : {}), }); + if (!session.instructionGraph.nestedScanComplete && !options.allowIncompleteInstructionDiscovery) { + return refused({ + code: "skill.instruction_scan_incomplete", + detail: "Nested AGENTS.md discovery stopped at a scan limit. Start from a smaller project directory so all local rules can be checked before local tools run", + }); + } // A skill the user NAMED that requires authority this session does not hold // does not run at all: the user asked for it, so a silent downgrade would // run something other than what was asked for. diff --git a/src/core/skills/skill_bounds.ts b/src/core/skills/skill_bounds.ts index cc5ebe0f..fbf79679 100644 --- a/src/core/skills/skill_bounds.ts +++ b/src/core/skills/skill_bounds.ts @@ -32,6 +32,10 @@ export const SKILL_BOUNDS = { maxContextPacketBytes: 512 * 1024, /** Nested AGENTS.md depth below the project root. */ maxNestedInstructionDepth: 6, + /** Bound the synchronous nested-rule walk before any chat pulse or request. */ + maxNestedInstructionDirectories: 256, + maxNestedInstructionEntries: 2048, + maxNestedInstructionScanMs: 500, /** One instruction file. */ maxInstructionFileBytes: 64 * 1024, /** Description / name / trigger phrase field lengths. */ diff --git a/src/core/skills/skill_errors.ts b/src/core/skills/skill_errors.ts index 0c2b6fb0..e87cbf35 100644 --- a/src/core/skills/skill_errors.ts +++ b/src/core/skills/skill_errors.ts @@ -13,6 +13,7 @@ export const SKILL_ERROR_CODES = [ "skill.dependency_missing", "skill.dependency_cycle", "skill.context_budget_exceeded", + "skill.instruction_scan_incomplete", "skill.tool_not_declared", "skill.permission_unavailable", "skill.permission_denied", diff --git a/src/core/stream.ts b/src/core/stream.ts index 7f79c242..c987b961 100644 --- a/src/core/stream.ts +++ b/src/core/stream.ts @@ -9,6 +9,16 @@ // be ignored. The CF-flush preamble (`:<4096 spaces>`) and `ping` heartbeat are // handled here (comment lines skipped; ping surfaced as a typed liveness frame). +export const STREAM_ERROR_NO_DETAILS = "cloud turn failed; the server did not provide details"; + +// `error` is not the Cloud SSE public-message contract. An older media loop +// emitted raw exception text under that key; only these route-owned fixed +// public strings may stand in for an absent `msg`/`message`. +const PUBLIC_LEGACY_ERROR_TEXT = new Set([ + "The media studio agent hit an error.", + "generation registry did not reach a terminal state", +]); + export type StreamFrame = StreamFrameBody & { /** Per-session monotonic sequence number (dev-session frames only). A * reconnecting client resumes with ?last_seq=N and MUST skip seq <= N so a @@ -127,7 +137,7 @@ function normalizeFrameBody(obj: Record): StreamFrameBody | nul case "error": return { type: "error", - msg: String(obj["msg"] ?? obj["message"] ?? ""), + msg: streamErrorMessage(obj), errorCode: strOrUndef(obj["error_code"] ?? obj["errorCode"] ?? obj["code"]), refId: strOrUndef(obj["ref_id"] ?? obj["refId"]), }; @@ -330,6 +340,19 @@ function numOrUndef(v: unknown): number | undefined { function strOrUndef(v: unknown): string | undefined { return v == null ? undefined : String(v); } +function streamErrorMessage(obj: Record): string { + for (const key of ["msg", "message"]) { + const value = obj[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + const alternate = obj["error"]; + if (typeof alternate === "string" && PUBLIC_LEGACY_ERROR_TEXT.has(alternate.trim())) { + return alternate.trim(); + } + // Never surface raw `error` or `reason`: historically these could carry + // provider exceptions or other internal details. + return STREAM_ERROR_NO_DETAILS; +} function parseStrArray(v: unknown): string[] | undefined { if (!Array.isArray(v)) return undefined; return v.map((x) => String(x)); diff --git a/src/core/transport.ts b/src/core/transport.ts index b4100944..f8472bf6 100644 --- a/src/core/transport.ts +++ b/src/core/transport.ts @@ -191,8 +191,15 @@ export class ApiClient { if (!res.ok) return false; let body: { session_token?: string } | undefined; try { - body = (await res.json()) as { session_token?: string }; + // The refresh fetch timeout also needs a bound after headers arrive. + // This promise is shared by concurrent 401 callers, so its body + // limit is independent of any one caller's AbortSignal. + const bodyTimeoutMs = Math.min(defaultRequestTimeoutMs() || 10_000, 10_000); + body = (await raceAgainst( + res.json(), undefined, bodyTimeoutMs, () => new RequestTimeoutError(bodyTimeoutMs), + )) as { session_token?: string }; } catch { + void res.body?.cancel().catch(() => {}); return false; } if (!body?.session_token) return false; @@ -279,7 +286,7 @@ export class ApiClient { void res.body?.cancel().catch(() => {}); res = await open(); } - if (!res.ok) throw await toHttpError(res); + if (!res.ok) throw await toHttpError(res, signal, timeoutMs); // Fail-soft: server returns plain JSON `{"stream": false}` instead of an // SSE body when it can't/shouldn't stream -> caller falls back to /agent/chat. const ct = res.headers.get("content-type") ?? ""; @@ -381,7 +388,7 @@ export class ApiClient { void res.body?.cancel().catch(() => {}); res = await send(); } - if (!res.ok) throw await toHttpError(res); + if (!res.ok) throw await toHttpError(res, signal, effTimeoutMs); if (!res.body) { cleanup(); return res; @@ -455,7 +462,7 @@ export class ApiClient { void res.body?.cancel().catch(() => {}); res = await send(); } - if (!res.ok) throw await toHttpError(res); + if (!res.ok) throw await toHttpError(res, signal, effTimeoutMs); return await parseOkBody(res); } finally { releaseNet(); @@ -511,7 +518,7 @@ export class ApiClient { void res.body?.cancel().catch(() => {}); res = await send(); } - if (!res.ok) throw await toHttpError(res); + if (!res.ok) throw await toHttpError(res, signal, timeoutMs); return await parseOkBody(res); } finally { releaseNet(); @@ -574,11 +581,21 @@ export function sanitizeServerText(v: string): string { return v.replace(/[\x00-\x1f\x7f-\x9f]+/g, " ").trim().slice(0, 200); } -async function toHttpError(res: Response): Promise { +async function toHttpError( + res: Response, + signal: AbortSignal | undefined, + timeoutMs: number, +): Promise { let body: unknown; try { - body = await res.json(); - } catch { + // Headers already tell us the status. Bound reading an optional detail + // body even when the caller disabled the normal stream idle timeout; a + // stalled error body must never hide a known HTTP 401 indefinitely. + const detailTimeoutMs = timeoutMs > 0 ? Math.min(timeoutMs, 3_000) : 3_000; + body = await raceAgainst(res.json(), signal, detailTimeoutMs, () => new RequestTimeoutError(detailTimeoutMs)); + } catch (error) { + if (signal?.aborted) throw abortError(signal); + if (error instanceof RequestTimeoutError) void res.body?.cancel().catch(() => {}); body = undefined; } // Surface the server's own explanation (FastAPI uses `detail`, others diff --git a/src/version.ts b/src/version.ts index 03be45b3..c0224003 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ // Single source of the CLI version (kept in lockstep with package.json). -export const VERSION = "0.3.2"; +export const VERSION = "0.3.3"; diff --git a/test/auth_401.test.ts b/test/auth_401.test.ts index 49b0c993..b2bc3b05 100644 --- a/test/auth_401.test.ts +++ b/test/auth_401.test.ts @@ -123,6 +123,33 @@ test("ApiClient: aek_ API keys never attempt refresh — the 401 surfaces direct } }); +test("ApiClient: a stalled 401 response body is bounded for JSON and stream requests", async () => { + const real = globalThis.fetch; + let bodyController: ReadableStreamDefaultController | undefined; + let calls = 0; + globalThis.fetch = (async () => { + calls++; + return new Response(new ReadableStream({ + start(controller) { bodyController = controller; }, + }), { status: 401, headers: { "content-type": "application/json" } }); + }) as typeof globalThis.fetch; + try { + const api = new ApiClient("https://api.example", new StaticTokenStore("aek_key")); + await assert.rejects(() => api.getJson("/models", undefined, 80), + (error: unknown) => error instanceof HttpError && error.status === 401); + bodyController?.close(); + bodyController = undefined; + await assert.rejects( + () => api.stream("/agent/chat/stream", {}, { timeoutMs: 80 }), + (error: unknown) => error instanceof HttpError && error.status === 401, + ); + assert.equal(calls, 2, "one request per call; no retry after a stalled detail body"); + } finally { + bodyController?.close(); + globalThis.fetch = real; + } +}); + test("ApiClient: failed refresh surfaces the ORIGINAL 401 (no retry loop)", async () => { const real = globalThis.fetch; const calls: Call[] = []; @@ -137,6 +164,32 @@ test("ApiClient: failed refresh surfaces the ORIGINAL 401 (no retry loop)", asyn } }); +test("ApiClient: stalled successful refresh body is bounded and surfaces original 401", async () => { + const real = globalThis.fetch; + const previousTimeout = process.env["AETHER_REQUEST_TIMEOUT_MS"]; + process.env["AETHER_REQUEST_TIMEOUT_MS"] = "80"; + const calls: Call[] = []; + let refreshBody: ReadableStreamDefaultController | undefined; + const store = new StaticTokenStore("sess_expired"); + stubFetch((url) => url.endsWith("/auth/refresh") + ? new Response(new ReadableStream({ start(controller) { refreshBody = controller; } }), { + status: 200, headers: { "content-type": "application/json" }, + }) + : jsonRes(401, { detail: "expired" }), calls); + try { + const api = new ApiClient("https://api.example", store); + await assert.rejects(() => api.getJson("/models"), + (error: unknown) => error instanceof HttpError && error.status === 401); + assert.deepEqual(calls.map((call) => call.url.replace("https://api.example", "")), ["/models", "/auth/refresh"]); + assert.equal(await store.get(), "sess_expired", "a stalled refresh must not replace the credential"); + } finally { + refreshBody?.close(); + globalThis.fetch = real; + if (previousTimeout === undefined) delete process.env["AETHER_REQUEST_TIMEOUT_MS"]; + else process.env["AETHER_REQUEST_TIMEOUT_MS"] = previousTimeout; + } +}); + test("ApiClient: a 401 from /auth/* itself never recurses into refresh", async () => { const real = globalThis.fetch; const calls: Call[] = []; diff --git a/test/chat.test.ts b/test/chat.test.ts index 9f2cb192..5b094003 100644 --- a/test/chat.test.ts +++ b/test/chat.test.ts @@ -1,9 +1,10 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { runTurn, ChatTurnError, applyRestart, buildPromptContext, repaintString } from "../src/commands/chat.js"; +import { cmdChat, runTurn, ChatTurnError, applyRestart, buildPromptContext, repaintString } from "../src/commands/chat.js"; import { handleSlash } from "../src/commands/slash.js"; import { ApiClient } from "../src/core/transport.js"; import { StreamIncompleteError } from "../src/core/errors.js"; +import { STREAM_ERROR_NO_DETAILS } from "../src/core/stream.js"; import type { GlobalFlags, AppContext } from "../src/core/context.js"; import type { TokenStore } from "../src/core/auth.js"; @@ -59,6 +60,40 @@ test("runTurn throws ChatTurnError when the server streams an error frame", asyn } }); +test("one-shot chat exits nonzero and renders only known public media errors or a fallback after done", async () => { + const realFetch = globalThis.fetch; + const realStderrWrite = process.stderr.write.bind(process.stderr); + let stderr = ""; + process.stderr.write = ((chunk: unknown): boolean => { + stderr += String(chunk); + return true; + }) as typeof process.stderr.write; + try { + for (const { wire, expected } of [ + { wire: { type: "error" }, expected: STREAM_ERROR_NO_DETAILS }, + { wire: { type: "error", msg: " " }, expected: STREAM_ERROR_NO_DETAILS }, + { wire: { type: "error", error: "The media studio agent hit an error.", reason: "private internal reason" }, expected: "The media studio agent hit an error." }, + { wire: { type: "error", error: "private provider failure: secret-key", reason: "private internal reason" }, expected: STREAM_ERROR_NO_DETAILS }, + ]) { + stderr = ""; + globalThis.fetch = sseFetch([ + JSON.stringify(wire), + JSON.stringify({ type: "done", uvt: 0, cents: 0 }), + ]); + const ctx = ctxWith(); + ctx.flags.json = false; + assert.equal(await cmdChat(ctx, "just testing reply 1"), 1); + assert.ok(stderr.includes(`✗ ${expected}`)); + assert.doesNotMatch(stderr, /\n✗\s*\n/, "the operator must not see a bare error glyph"); + assert.doesNotMatch(stderr, /private internal reason/, "the private reason must not reach the terminal"); + assert.doesNotMatch(stderr, /private provider failure|secret-key/, "an arbitrary error field must not reach the terminal"); + } + } finally { + globalThis.fetch = realFetch; + process.stderr.write = realStderrWrite; + } +}); + test("runTurn resolves cleanly on a clean stream", async () => { const real = globalThis.fetch; globalThis.fetch = sseFetch([ diff --git a/test/generated_docs.test.ts b/test/generated_docs.test.ts index ae903a7e..c9354121 100644 --- a/test/generated_docs.test.ts +++ b/test/generated_docs.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from "node:os"; import { COMMAND_MANIFEST, type CommandManifestEntry } from "../src/commands/command_manifest.js"; import { PUBLIC_CATALOGUE_SCHEMA, + CATALOGUE_MAX_AGE_MS, buildGeneratedOutputs, generateDocumentation, parseCatalogue, @@ -17,7 +18,7 @@ import { deterministicRepositoryEvidence } from "../scripts/release-truth.js"; const unsignedSource = { schema: PUBLIC_CATALOGUE_SCHEMA, sourceVersion: "cloud-test-v1", - generatedAt: "2026-08-23T00:00:00.000Z", + generatedAt: new Date().toISOString(), availabilitySemantics: "listed-not-entitled", scopeNote: "A sanitized, dated subset; live availability remains account-scoped.", models: [ @@ -101,7 +102,7 @@ test("Cloud projection digest, freshness, schema, and safe model fields are mand const genericUnsigned = { ...unsignedSource, models: [{ ...source.models[0], id: "model" }] }; const generic = { ...genericUnsigned, digest: sha256(sourceContent(genericUnsigned)) }; assert.throws(() => generateDocumentation({ root, catalogueSourceText: JSON.stringify(generic) }), /invalid or generic id/); - const staleUnsigned = { ...unsignedSource, generatedAt: "2026-01-01T00:00:00.000Z" }; + const staleUnsigned = { ...unsignedSource, generatedAt: new Date(Date.now() - CATALOGUE_MAX_AGE_MS - 86_400_000).toISOString() }; assert.throws(() => generateDocumentation({ root, catalogueSourceText: JSON.stringify({ ...staleUnsigned, digest: sha256(sourceContent(staleUnsigned)) }) }), /projection is stale/); assert.doesNotThrow(() => generateDocumentation({ root, catalogueLiveSourceText: "network unavailable" })); const fallback = JSON.parse(readFileSync(join(root, "docs", "model-catalogue", "catalogue.json"), "utf8")) as Record; @@ -117,7 +118,7 @@ test("Cloud content digests are stable across generatedAt and unsupported row fi assert.throws(() => parseCatalogue(JSON.stringify(withHosting), Date.parse(source.generatedAt)), /unsupported fields: hosting/); }); -test("checked-in Cloud #1327 projection remains compatible with the Agent consumer contract", () => { +test("historical Cloud #1327 provenance stays recorded while the refreshed projection satisfies the consumer contract", () => { const text = readFileSync(join(process.cwd(), "docs", "model-catalogue", "catalogue.source.json"), "utf8"); const raw = JSON.parse(text) as { generatedAt: string }; const catalogue = parseCatalogue(text, Date.parse(raw.generatedAt)); @@ -131,11 +132,9 @@ test("checked-in Cloud #1327 projection remains compatible with the Agent consum { head: CLOUD_1327_HEAD, merge: CLOUD_1327_MERGE, tree: CLOUD_1327_TREE, digest: CLOUD_1327_DIGEST }, "Cloud catalogue evidence must bind the declared head, landed merge, tree, and projection digest", ); - assert.equal(catalogue.digest, evidence[4], `Cloud #1327 ${CLOUD_1327_HEAD} fixture digest drifted`); - assert.equal(catalogue.models.length, 51); + assert.ok(catalogue.models.length > 0); const safeFields = ["availability", "id", "kind", "label", "modality", "provider", "tierMin"]; for (const model of catalogue.models) assert.deepEqual(Object.keys(model).sort(), safeFields); - assert.equal(catalogue.models.find((model) => model.id === "aether-vision")?.availability, "unavailable"); }); test("future timestamps and hostile public strings are rejected without leaking their values", () => { diff --git a/test/instruction_resolver.test.ts b/test/instruction_resolver.test.ts index 1d1b2075..beee965e 100644 --- a/test/instruction_resolver.test.ts +++ b/test/instruction_resolver.test.ts @@ -13,6 +13,7 @@ import { sourceAppliesTo, } from "../src/core/instructions/instruction_resolver.js"; import { SKILL_BOUNDS } from "../src/core/skills/skill_bounds.js"; +import { openRunSession } from "../src/core/skills/run_session.js"; import type { InstructionSource } from "../src/core/instructions/instruction_types.js"; function withEnv(key: string, value: string, fn: () => T): T { @@ -66,6 +67,37 @@ test("nested AGENTS.md scopes to its subtree only", () => { }); }); +test("a wide project stops nested discovery visibly and refuses local tool authority", () => { + const root = makeProject(); + writeFileSync(join(root, "AGENTS.md"), "Keep the root rule.\n"); + const userDir = mkdtempSync(join(tmpdir(), "aether-cfg-")); + writeFileSync(join(userDir, "instructions.md"), "Keep the user rule.\n"); + // The old depth-only walk could spend minutes traversing a home directory + // before even starting the chat pulse. This exceeds the directory budget + // without depending on a machine-specific stopwatch threshold. + for (let i = 0; i <= SKILL_BOUNDS.maxNestedInstructionDirectories; i++) { + mkdirSync(join(root, `child-${String(i).padStart(4, "0")}`)); + } + withEnv("AETHER_CONFIG_DIR", userDir, () => { + const graph = resolveInstructionGraph(root); + assert.equal(graph.nestedScanComplete, false); + assert.ok(graph.sources.some((source) => source.kind === "agents-root" && source.content.includes("Keep the root rule"))); + assert.ok(graph.sources.some((source) => source.kind === "aether-user" && source.content.includes("Keep the user rule"))); + assert.ok(graph.skipped.some((item) => item.path === "**/AGENTS.md" && item.reason.includes("may be missing"))); + + const local = openRunSession({ projectRoot: root, prompt: "edit files" }); + assert.equal(local.ok, false); + if (!local.ok) assert.equal(local.refusal.code, "skill.instruction_scan_incomplete"); + + const cloudChat = openRunSession({ projectRoot: root, prompt: "reply 1", allowIncompleteInstructionDiscovery: true }); + assert.equal(cloudChat.ok, true, cloudChat.ok ? "" : cloudChat.lines.join("\n")); + if (cloudChat.ok) { + assert.equal(cloudChat.run.hasWarnings, true); + assert.match(cloudChat.run.headerLines.join("\n"), /nested project rules may be missing/); + } + }); +}); + test("precedence: canonical Aether project instruction beats root AGENTS.md", () => { const root = makeProject(); writeFileSync(join(root, "AGENTS.md"), "Always run `npm test`.\n"); diff --git a/test/public_docs_truth.test.ts b/test/public_docs_truth.test.ts index 986b61cb..8ebb340f 100644 --- a/test/public_docs_truth.test.ts +++ b/test/public_docs_truth.test.ts @@ -126,13 +126,16 @@ test("README states npm and source versions in a way publishing cannot falsify", assert.match(npmCell, /img\.shields\.io\/npm\/v\/aether-agents/, "the npm `latest` cell must resolve the live dist-tag"); assert.doesNotMatch(readme, /future published/i, "README asserts an unpublished state that publishing invalidates"); - assert.ok(readme.includes(`| ${tick}main${tick} source build | **${sourceVersion}** |`)); + assert.ok(readme.includes(`| ${tick}release/0.3${tick} source | **${sourceVersion}** |`)); const sourceStart = readme.indexOf(""); const sourceEnd = readme.indexOf(""); assert.ok(sourceStart >= 0 && sourceEnd > sourceStart, "README source-only scope markers are missing"); const sourceScope = readme.slice(sourceStart, sourceEnd); - assert.match(sourceScope, new RegExp(`Requires ${sourceVersion.replaceAll(".", "\\.")} or newer`)); + // The workflows in this section first shipped in 0.3.2; a maintenance + // candidate may advance its own version without rewriting that truthful + // minimum installed version. + assert.match(sourceScope, /Requires 0\.3\.2 or newer/); }); test("README fenced shell examples use registered commands and flags", () => { diff --git a/test/release_coherence.test.ts b/test/release_coherence.test.ts index 36210e84..67b9aabf 100644 --- a/test/release_coherence.test.ts +++ b/test/release_coherence.test.ts @@ -68,6 +68,14 @@ const V032_PACKET = { "Package manifest": "The same current local dry run reported 673 entries. Hosted exact-head package evidence remains required before release.", "Provenance evidence": "Pending the trusted-publishing workflow — no v0.3.2 provenance attestation exists yet.", } as const; +const V033_PACKET = { + "Package": "`aether-agents`", + "Proposed tag": "`v0.3.3`", + "Release line base": "`e234bf6bcd283dd81691158e70e826a47013eea6` (`release/0.3`, synced to published v0.3.2)", + "Candidate branch": "`fix/032-home-scan`", + "Archive evidence": "No v0.3.3 release archive or published npm package exists. Exact-head hosted package evidence is pending.", + "Provenance evidence": "Pending a future trusted-publishing workflow; no v0.3.3 provenance attestation is claimed.", +} as const; function parsePacketRows(packet: string): PacketRows { const rows: PacketRows = new Map(); @@ -200,9 +208,14 @@ test("the current operator packet identifies the candidate and v0.3.0 retains it const current = readFileSync(path, "utf8"); assert.ok(current.includes(`v${VERSION}`), "the operator packet does not name the proposed tag"); const currentRows = parsePacketRows(current); - for (const [label, expected] of Object.entries(V032_PACKET)) { + for (const [label, expected] of Object.entries(V033_PACKET)) { assert.equal(onlyPacketRow(currentRows, label), expected, `the current packet has the wrong ${label}`); } + const prior = read("docs", "releases", "OPERATOR-PACKET-v0.3.2.md"); + const priorRows = parsePacketRows(prior); + for (const [label, expected] of Object.entries(V032_PACKET)) { + assert.equal(onlyPacketRow(priorRows, label), expected, `the v0.3.2 packet has the wrong ${label}`); + } assert.doesNotMatch(current, /aether-agents-0\.3\.0\.tgz|6176172deb15eea57519408d93f23b3fac8ab5e2b2e541adddc34b4e5fb4c33d/); const packet = read("docs", "releases", "OPERATOR-PACKET-v0.3.0.md"); assertV030PacketProvenance(packet); diff --git a/test/release_truth.test.ts b/test/release_truth.test.ts index 4f76fd24..3dd3a8e4 100644 --- a/test/release_truth.test.ts +++ b/test/release_truth.test.ts @@ -349,6 +349,11 @@ test("public registry claims are derived from packed docs and reject pre-publish }); assert.equal(live.sourceVersion, "0.3.0"); assert.deepEqual(live.pinnedRegistryClaims, []); + const maintenance = derivePublicRegistryClaim({ + "README.md": "| `release/0.3` source | **0.3.3** | Check the live npm badge for the published package version. |", + }); + assert.equal(maintenance.sourceVersion, "0.3.3"); + assert.deepEqual(maintenance.pinnedRegistryClaims, []); }); test("the shipped README states npm status in a way publishing cannot falsify", () => {