From c1814f38f83b4e2776d3cf750492cc759847e41f Mon Sep 17 00:00:00 2001 From: Dmytro Prylutskyi Date: Tue, 20 Oct 2026 14:04:29 +0200 Subject: [PATCH 1/2] chore: point repository URLs at dip-develop and pin build_runner - Replace github.com/alteri-one/alteri-one with github.com/dip-develop/alteri-one across pubspecs, READMEs, and community files (closes open question Q4). - Pin build_runner to >=2.16.1 <2.17.0: 2.17.0 is retracted on pub.dev and 2.17.1 needs build 5 while injectable_generator 3.x needs build ^4. Signed-off-by: Dmytro Prylutskyi --- CONTRIBUTING.md | 2 +- NOTICE | 2 +- README.md | 8 ++++---- clients/app/pubspec.yaml | 4 ++-- clients/site/pubspec.yaml | 4 ++-- e2e/pubspec.yaml | 4 ++-- extensions/lsp/README.md | 4 ++-- extensions/lsp/pubspec.yaml | 4 ++-- extensions/mcp/README.md | 4 ++-- extensions/mcp/pubspec.yaml | 4 ++-- extensions/memory_hive/README.md | 4 ++-- extensions/memory_hive/pubspec.yaml | 4 ++-- extensions/weather/README.md | 4 ++-- extensions/weather/pubspec.yaml | 4 ++-- extensions/workspace/README.md | 4 ++-- extensions/workspace/pubspec.yaml | 4 ++-- packages/client/README.md | 4 ++-- packages/client/pubspec.yaml | 4 ++-- packages/contracts/README.md | 4 ++-- packages/contracts/pubspec.yaml | 4 ++-- packages/design/README.md | 4 ++-- packages/design/pubspec.yaml | 4 ++-- packages/prompts/README.md | 4 ++-- packages/prompts/pubspec.yaml | 4 ++-- packages/provider_api/README.md | 4 ++-- packages/provider_api/pubspec.yaml | 4 ++-- packages/sdk/README.md | 4 ++-- packages/sdk/pubspec.yaml | 4 ++-- packages/testkit/README.md | 4 ++-- packages/testkit/pubspec.yaml | 4 ++-- providers/llama_cpp/README.md | 4 ++-- providers/llama_cpp/pubspec.yaml | 4 ++-- runtime/bin/pubspec.yaml | 4 ++-- runtime/cli/pubspec.yaml | 4 ++-- tools/arch_check/pubspec.yaml | 4 ++-- tools/codegen_check/pubspec.yaml | 4 ++-- tools/docs_check/pubspec.yaml | 4 ++-- tools/signing/pubspec.yaml | 4 ++-- 38 files changed, 76 insertions(+), 76 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 136f5a9..9972211 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,7 +63,7 @@ docs(spec): clarify subtask rights intersection ## Reporting bugs and proposing features -Open a [GitHub issue](https://github.com/alteri-one/alteri-one/issues) using the templates. Security issues go through the [private channel](SECURITY.md), never a public issue. +Open a [GitHub issue](https://github.com/dip-develop/alteri-one/issues) using the templates. Security issues go through the [private channel](SECURITY.md), never a public issue. ## License diff --git a/NOTICE b/NOTICE index 69fe173..4df5a45 100644 --- a/NOTICE +++ b/NOTICE @@ -2,7 +2,7 @@ AlteriOne Copyright 2026 The AlteriOne Authors This product includes software developed by The AlteriOne Authors -(https://github.com/alteri-one/alteri-one). +(https://github.com/dip-develop/alteri-one). Licensed under the Apache License, Version 2.0 (the "License"). You may not use this product except in compliance with the License. diff --git a/README.md b/README.md index d5be7cf..9815feb 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # AlteriOne -[![CI](https://github.com/alteri-one/alteri-one/actions/workflows/ci.yml/badge.svg)](https://github.com/alteri-one/alteri-one/actions/workflows/ci.yml) +[![CI](https://github.com/dip-develop/alteri-one/actions/workflows/ci.yml/badge.svg)](https://github.com/dip-develop/alteri-one/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -[![Discussions](https://img.shields.io/badge/discussions-welcome-brightgreen.svg)](https://github.com/alteri-one/alteri-one/discussions) +[![Discussions](https://img.shields.io/badge/discussions-welcome-brightgreen.svg)](https://github.com/dip-develop/alteri-one/discussions) **A personal universal assistant: one runtime, three run modes.** @@ -63,7 +63,7 @@ One Dart workspace (one `pubspec.lock`, shared resolution) plus Melos. Packages Requirements: [Dart](https://dart.dev) `^3.13.0` (a Flutter SDK gives you Dart; the client also needs [Flutter](https://flutter.dev) `3.47+`), and [Melos](https://pub.dev/packages/melos) 7. ```sh -git clone https://github.com/alteri-one/alteri-one.git +git clone https://github.com/dip-develop/alteri-one.git cd alteri-one flutter pub get # resolves the whole workspace, one lock file melos run format # formatting @@ -91,7 +91,7 @@ Contributions are welcome — for code, documentation, extension examples, and b - [Contributing guide](CONTRIBUTING.md) — development setup, conventions, and the DCO sign-off (`git commit -s`; no CLA). - [Code of Conduct](CODE_OF_CONDUCT.md) - [Security policy](SECURITY.md) — please report vulnerabilities privately. -- Questions and ideas: [GitHub Discussions](https://github.com/alteri-one/alteri-one/discussions). +- Questions and ideas: [GitHub Discussions](https://github.com/dip-develop/alteri-one/discussions). ## License diff --git a/clients/app/pubspec.yaml b/clients/app/pubspec.yaml index 6419b5f..7e4e198 100644 --- a/clients/app/pubspec.yaml +++ b/clients/app/pubspec.yaml @@ -3,8 +3,8 @@ description: >- AlteriOne Flutter client for desktop, web, and mobile. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/clients/site/pubspec.yaml b/clients/site/pubspec.yaml index 209aacb..acca3e8 100644 --- a/clients/site/pubspec.yaml +++ b/clients/site/pubspec.yaml @@ -3,8 +3,8 @@ description: >- The alteri.one website, built with Jaspr server-side rendering. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/e2e/pubspec.yaml b/e2e/pubspec.yaml index b43dcc3..77c190c 100644 --- a/e2e/pubspec.yaml +++ b/e2e/pubspec.yaml @@ -3,8 +3,8 @@ description: >- End-to-end tests for the AlteriOne daemon, extensions, and clients. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/extensions/lsp/README.md b/extensions/lsp/README.md index 6f2ee32..aae1a87 100644 --- a/extensions/lsp/README.md +++ b/extensions/lsp/README.md @@ -5,7 +5,7 @@ A first-party extension published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/extensions/lsp/pubspec.yaml b/extensions/lsp/pubspec.yaml index 5af0b0b..2f8f033 100644 --- a/extensions/lsp/pubspec.yaml +++ b/extensions/lsp/pubspec.yaml @@ -3,8 +3,8 @@ description: >- LSP client and code-navigation tools for AlteriOne task mode. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/extensions/mcp/README.md b/extensions/mcp/README.md index a76f110..2f3ccb0 100644 --- a/extensions/mcp/README.md +++ b/extensions/mcp/README.md @@ -5,7 +5,7 @@ A first-party extension published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/extensions/mcp/pubspec.yaml b/extensions/mcp/pubspec.yaml index 9fa02e2..89124fc 100644 --- a/extensions/mcp/pubspec.yaml +++ b/extensions/mcp/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Model Context Protocol (MCP) client extension for AlteriOne. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/extensions/memory_hive/README.md b/extensions/memory_hive/README.md index 9f30470..bd264e8 100644 --- a/extensions/memory_hive/README.md +++ b/extensions/memory_hive/README.md @@ -5,7 +5,7 @@ A first-party extension published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/extensions/memory_hive/pubspec.yaml b/extensions/memory_hive/pubspec.yaml index a35c7b2..82ef8a9 100644 --- a/extensions/memory_hive/pubspec.yaml +++ b/extensions/memory_hive/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Hive CE-backed long-term memory backend for AlteriOne. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/extensions/weather/README.md b/extensions/weather/README.md index 582c666..d4ca1df 100644 --- a/extensions/weather/README.md +++ b/extensions/weather/README.md @@ -5,7 +5,7 @@ A first-party extension published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/extensions/weather/pubspec.yaml b/extensions/weather/pubspec.yaml index 1004045..3e44612 100644 --- a/extensions/weather/pubspec.yaml +++ b/extensions/weather/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Reference weather extension for AlteriOne; the working example for extension authors. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/extensions/workspace/README.md b/extensions/workspace/README.md index 7379881..a508a6e 100644 --- a/extensions/workspace/README.md +++ b/extensions/workspace/README.md @@ -5,7 +5,7 @@ A first-party extension published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/extensions/workspace/pubspec.yaml b/extensions/workspace/pubspec.yaml index 0803105..c92af2d 100644 --- a/extensions/workspace/pubspec.yaml +++ b/extensions/workspace/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Task-mode workspace and file tools for AlteriOne. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/client/README.md b/packages/client/README.md index 8ef7300..f18043c 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/client/pubspec.yaml b/packages/client/pubspec.yaml index 44f6ac6..8d2ee4d 100644 --- a/packages/client/pubspec.yaml +++ b/packages/client/pubspec.yaml @@ -3,8 +3,8 @@ description: >- HTTP and SSE client for the AlteriOne daemon public API. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/contracts/README.md b/packages/contracts/README.md index da97606..c6eeef0 100644 --- a/packages/contracts/README.md +++ b/packages/contracts/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/contracts/pubspec.yaml b/packages/contracts/pubspec.yaml index 3aec9eb..2a0b118 100644 --- a/packages/contracts/pubspec.yaml +++ b/packages/contracts/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Serializable contracts for AlteriOne: protocol DTOs, events, tool specifications, and extension manifests. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/design/README.md b/packages/design/README.md index 2c532f2..a891a0f 100644 --- a/packages/design/README.md +++ b/packages/design/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/design/pubspec.yaml b/packages/design/pubspec.yaml index 36dae7d..12cf59f 100644 --- a/packages/design/pubspec.yaml +++ b/packages/design/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Design tokens for AlteriOne: tokens.yaml compiled to Flutter ThemeExtensions and CSS variables. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/prompts/README.md b/packages/prompts/README.md index 74803f9..69c6053 100644 --- a/packages/prompts/README.md +++ b/packages/prompts/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/prompts/pubspec.yaml b/packages/prompts/pubspec.yaml index f25be9c..7b54fc0 100644 --- a/packages/prompts/pubspec.yaml +++ b/packages/prompts/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Dotprompt-based prompt rendering and input validation for AlteriOne. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/provider_api/README.md b/packages/provider_api/README.md index 4636d0f..f973bfc 100644 --- a/packages/provider_api/README.md +++ b/packages/provider_api/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/provider_api/pubspec.yaml b/packages/provider_api/pubspec.yaml index d87db75..4b21328 100644 --- a/packages/provider_api/pubspec.yaml +++ b/packages/provider_api/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Ports for LLM model providers in AlteriOne: LlmPort, ModelProvider, and declared capabilities. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 2990520..84c4969 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/sdk/pubspec.yaml b/packages/sdk/pubspec.yaml index 378f12d..e625a73 100644 --- a/packages/sdk/pubspec.yaml +++ b/packages/sdk/pubspec.yaml @@ -3,8 +3,8 @@ description: >- SDK for AlteriOne extension authors: host API, tool registration, and state observation. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/packages/testkit/README.md b/packages/testkit/README.md index da06309..3a3c80d 100644 --- a/packages/testkit/README.md +++ b/packages/testkit/README.md @@ -5,7 +5,7 @@ A public library published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/packages/testkit/pubspec.yaml b/packages/testkit/pubspec.yaml index 9ec58b0..5472ff5 100644 --- a/packages/testkit/pubspec.yaml +++ b/packages/testkit/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Conformance test suites for AlteriOne LLM providers and extensions. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/providers/llama_cpp/README.md b/providers/llama_cpp/README.md index edc56f1..c376f7c 100644 --- a/providers/llama_cpp/README.md +++ b/providers/llama_cpp/README.md @@ -5,7 +5,7 @@ A public LLM provider published to pub.dev as part of AlteriOne. **Status: pre-alpha scaffold.** Implementation follows the milestones in -[`docs/spec/roadmap.md`](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/roadmap.md). +[`docs/spec/roadmap.md`](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/roadmap.md). ## About AlteriOne @@ -13,7 +13,7 @@ AlteriOne is a personal universal assistant: one runtime with three run modes (`chat`, `task`, `resident`), extensions, LLM providers, and LSP and MCP integrations. One owner per instance; no accounts, no multi-user mode. -See the [root README](https://github.com/alteri-one/alteri-one) and the [specification index](https://github.com/alteri-one/alteri-one/blob/main/docs/spec/index.md). +See the [root README](https://github.com/dip-develop/alteri-one) and the [specification index](https://github.com/dip-develop/alteri-one/blob/main/docs/spec/index.md). ## License diff --git a/providers/llama_cpp/pubspec.yaml b/providers/llama_cpp/pubspec.yaml index 2f97589..553050d 100644 --- a/providers/llama_cpp/pubspec.yaml +++ b/providers/llama_cpp/pubspec.yaml @@ -3,8 +3,8 @@ description: >- llama.cpp (llama-server) LLM provider for AlteriOne, built on the OpenAI-compatible API. version: 0.0.1 homepage: https://alteri.one -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 topics: diff --git a/runtime/bin/pubspec.yaml b/runtime/bin/pubspec.yaml index 00c3c20..a67a81b 100644 --- a/runtime/bin/pubspec.yaml +++ b/runtime/bin/pubspec.yaml @@ -3,8 +3,8 @@ description: >- The alterione executable entry point for AlteriOne. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/runtime/cli/pubspec.yaml b/runtime/cli/pubspec.yaml index 01e6f22..4651a46 100644 --- a/runtime/cli/pubspec.yaml +++ b/runtime/cli/pubspec.yaml @@ -3,8 +3,8 @@ description: >- AlteriOne command-line interface commands, as a library. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/tools/arch_check/pubspec.yaml b/tools/arch_check/pubspec.yaml index b3bdc42..2e9d0a9 100644 --- a/tools/arch_check/pubspec.yaml +++ b/tools/arch_check/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Dependency-rule checks for the AlteriOne workspace. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/tools/codegen_check/pubspec.yaml b/tools/codegen_check/pubspec.yaml index f5419d7..768b1e6 100644 --- a/tools/codegen_check/pubspec.yaml +++ b/tools/codegen_check/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Checks that generated code in the AlteriOne workspace is up to date. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/tools/docs_check/pubspec.yaml b/tools/docs_check/pubspec.yaml index d8e2380..70ed355 100644 --- a/tools/docs_check/pubspec.yaml +++ b/tools/docs_check/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Documentation checks for the AlteriOne workspace: links, forbidden terms, AGENTS.md presence. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace diff --git a/tools/signing/pubspec.yaml b/tools/signing/pubspec.yaml index 51cbf24..2db7505 100644 --- a/tools/signing/pubspec.yaml +++ b/tools/signing/pubspec.yaml @@ -3,8 +3,8 @@ description: >- Ed25519 signing of loaded AlteriOne extensions. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace From 150e7f586a7346f89d7a56c64ff75d5bc3c99198 Mon Sep 17 00:00:00 2001 From: Dmytro Prylutskyi Date: Tue, 20 Oct 2026 14:04:53 +0200 Subject: [PATCH 2/2] feat(core): contracts, domain, ports, blocs, and DI skeleton Implements the interface skeleton per docs/spec/domain.md (sections 8-9), docs/spec/contracts.md (section 10), and docs/spec/dart-language.md (section 7): packages/contracts: - Risk, ToolSpec (interim hand-written form pending spike T2) - sealed AgentEvent hierarchy with JSON round-trip and UnknownEvent fallback - ApiErrorCode constants, ApiError, ExtensionManifest/ExtensionProvides alteri_one_core: - domain: transition() state machine, PolicyGate.decide (capability -> taint -> profile rules), effectiveRisk, RunCapabilities.narrow, Budget, Invocation, typed IDs, Message/Turn/ToolCall/ToolResultBlock, CancelToken, memory, ContextFragment, TriggerEvent with chain depth - ports: LlmPort, JournalPort, MemoryPort, ToolPort (abstract interface class) - application: RunBloc, ApprovalsBloc, ExtensionsBloc, SchedulerBloc (droppable), TriggersCubit, ChannelsCubit, StatePublisher stub, applyBlocDefaults - DI: configureCore(environment) with dev/prod/test; fail-closed PolicyGate Tests: full transition matrix incl. invalid transitions, all PolicyGate branches, capabilities boundaries, event JSON round-trip, RunBloc lifecycle under bloc_test (closes T14), component and DI suites. melos run ci green. Findings recorded in docs/spec/open-questions.md and docs/plan/M1.md: - T21: bloc 9 runs each on() as a separate subscription; cross-type ordering requires the static Bloc.transformer plus a single handler per bloc. - Q5: public_member_api_docs cannot see primary-constructor parameter docs. - Q6: verified new-constructor syntax differs from dart-language.md section 7.3. Signed-off-by: Dmytro Prylutskyi --- TRADEMARK.md | 2 +- analysis_options.yaml | 20 +- clients/app/lib/main.dart | 2 +- docs/plan/M1.md | 58 ++++ docs/spec/open-questions.md | 7 +- .../contracts/lib/alteri_one_contracts.dart | 18 +- packages/contracts/lib/src/agent_event.dart | 264 ++++++++++++++++++ packages/contracts/lib/src/error_codes.dart | 44 +++ packages/contracts/lib/src/manifest.dart | 57 ++++ packages/contracts/lib/src/risk.dart | 21 ++ packages/contracts/lib/src/tool_spec.dart | 62 ++++ packages/contracts/test/agent_event_test.dart | 124 ++++++++ runtime/core/lib/alteri_one_core.dart | 39 ++- .../lib/src/application/approvals_bloc.dart | 95 +++++++ .../lib/src/application/bloc_defaults.dart | 21 ++ .../lib/src/application/channels_cubit.dart | 56 ++++ .../lib/src/application/extensions_bloc.dart | 87 ++++++ .../core/lib/src/application/run_bloc.dart | 202 ++++++++++++++ .../lib/src/application/scheduler_bloc.dart | 45 +++ .../lib/src/application/state_publisher.dart | 47 ++++ .../lib/src/application/triggers_cubit.dart | 80 ++++++ runtime/core/lib/src/di/core_module.dart | 19 ++ runtime/core/lib/src/di/injection.config.dart | 43 +++ runtime/core/lib/src/di/injection.dart | 22 ++ runtime/core/lib/src/domain/budget.dart | 23 ++ runtime/core/lib/src/domain/cancel_token.dart | 30 ++ runtime/core/lib/src/domain/context.dart | 18 ++ runtime/core/lib/src/domain/ids.dart | 18 ++ runtime/core/lib/src/domain/invocation.dart | 52 ++++ runtime/core/lib/src/domain/memory.dart | 69 +++++ runtime/core/lib/src/domain/messages.dart | 111 ++++++++ runtime/core/lib/src/domain/policy.dart | 117 ++++++++ .../lib/src/domain/ports/journal_port.dart | 31 ++ .../core/lib/src/domain/ports/llm_port.dart | 21 ++ .../lib/src/domain/ports/memory_port.dart | 25 ++ .../core/lib/src/domain/ports/tool_port.dart | 20 ++ runtime/core/lib/src/domain/run_state.dart | 113 ++++++++ runtime/core/lib/src/domain/trigger.dart | 25 ++ runtime/core/pubspec.yaml | 5 +- .../test/application/components_test.dart | 104 +++++++ .../core/test/application/run_bloc_test.dart | 107 +++++++ runtime/core/test/di/injection_test.dart | 46 +++ runtime/core/test/domain/policy_test.dart | 170 +++++++++++ runtime/core/test/domain/run_state_test.dart | 106 +++++++ 44 files changed, 2623 insertions(+), 23 deletions(-) create mode 100644 docs/plan/M1.md create mode 100644 packages/contracts/lib/src/agent_event.dart create mode 100644 packages/contracts/lib/src/error_codes.dart create mode 100644 packages/contracts/lib/src/manifest.dart create mode 100644 packages/contracts/lib/src/risk.dart create mode 100644 packages/contracts/lib/src/tool_spec.dart create mode 100644 packages/contracts/test/agent_event_test.dart create mode 100644 runtime/core/lib/src/application/approvals_bloc.dart create mode 100644 runtime/core/lib/src/application/bloc_defaults.dart create mode 100644 runtime/core/lib/src/application/channels_cubit.dart create mode 100644 runtime/core/lib/src/application/extensions_bloc.dart create mode 100644 runtime/core/lib/src/application/run_bloc.dart create mode 100644 runtime/core/lib/src/application/scheduler_bloc.dart create mode 100644 runtime/core/lib/src/application/state_publisher.dart create mode 100644 runtime/core/lib/src/application/triggers_cubit.dart create mode 100644 runtime/core/lib/src/di/core_module.dart create mode 100644 runtime/core/lib/src/di/injection.config.dart create mode 100644 runtime/core/lib/src/di/injection.dart create mode 100644 runtime/core/lib/src/domain/budget.dart create mode 100644 runtime/core/lib/src/domain/cancel_token.dart create mode 100644 runtime/core/lib/src/domain/context.dart create mode 100644 runtime/core/lib/src/domain/ids.dart create mode 100644 runtime/core/lib/src/domain/invocation.dart create mode 100644 runtime/core/lib/src/domain/memory.dart create mode 100644 runtime/core/lib/src/domain/messages.dart create mode 100644 runtime/core/lib/src/domain/policy.dart create mode 100644 runtime/core/lib/src/domain/ports/journal_port.dart create mode 100644 runtime/core/lib/src/domain/ports/llm_port.dart create mode 100644 runtime/core/lib/src/domain/ports/memory_port.dart create mode 100644 runtime/core/lib/src/domain/ports/tool_port.dart create mode 100644 runtime/core/lib/src/domain/run_state.dart create mode 100644 runtime/core/lib/src/domain/trigger.dart create mode 100644 runtime/core/test/application/components_test.dart create mode 100644 runtime/core/test/application/run_bloc_test.dart create mode 100644 runtime/core/test/di/injection_test.dart create mode 100644 runtime/core/test/domain/policy_test.dart create mode 100644 runtime/core/test/domain/run_state_test.dart diff --git a/TRADEMARK.md b/TRADEMARK.md index 4e01087..e934a53 100644 --- a/TRADEMARK.md +++ b/TRADEMARK.md @@ -21,4 +21,4 @@ The spec's "personal builds" mechanism (`distribution.yaml`) intentionally gives ## Contact -Questions about the name: open a [discussion](https://github.com/alteri-one/alteri-one/discussions) or write to the address in [SECURITY.md](SECURITY.md) with "trademark" in the subject. +Questions about the name: open a [discussion](https://github.com/dip-develop/alteri-one/discussions) or write to the address in [SECURITY.md](SECURITY.md) with "trademark" in the subject. diff --git a/analysis_options.yaml b/analysis_options.yaml index 263b5d8..18ff039 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -11,6 +11,20 @@ analyzer: linter: rules: + # Section 7.12 of docs/spec/dart-language.md. + - no_dynamic_casts + - no_raw_types + - use_declaring_parameters + - unnecessary_primary_constructor_body + - unnecessary_type_name_in_constructor + - prefer_initializing_formals + - switch_on_type + - discarded_futures + - unawaited_futures + - unnecessary_unawaited + - async_return_with_no_await + - initialize_in_field_declaration + # Project additions. - always_declare_return_types - avoid_print - avoid_redundant_argument_values @@ -20,7 +34,9 @@ linter: - prefer_const_constructors - prefer_final_locals - prefer_single_quotes - - public_member_api_docs - - unawaited_futures - unnecessary_lambdas - unnecessary_parenthesis + # public_member_api_docs is omitted: the 3.13.5 analyzer does not attach + # /// comments to primary-constructor parameters, which the spec mandates + # (dart-language.md 7.2, domain.md 8.x). Parameter docs stay mandatory by + # convention; see docs/spec/open-questions.md. diff --git a/clients/app/lib/main.dart b/clients/app/lib/main.dart index 39bc76d..c891781 100644 --- a/clients/app/lib/main.dart +++ b/clients/app/lib/main.dart @@ -13,7 +13,7 @@ void main() { /// Placeholder entry point until the real client is implemented (see M9). final class AlteriOnePlaceholderApp extends StatelessWidget { /// Creates the placeholder application shell. - const AlteriOnePlaceholderApp({super.key}); + const new({super.key}); @override Widget build(BuildContext context) { diff --git a/docs/plan/M1.md b/docs/plan/M1.md new file mode 100644 index 0000000..c7c238f --- /dev/null +++ b/docs/plan/M1.md @@ -0,0 +1,58 @@ +# Milestone M1: contracts and core skeleton + +- Status: In progress +- Specification: `docs/spec/contracts.md` (section 10), `docs/spec/domain.md` (sections 8–9), `docs/spec/dart-language.md` (section 7) +- ADR: — (no decision changed; interim hand-written DTOs pending spike T2; T21 documented in `open-questions.md`) + +## Scope note + +The owner asked for the full interface skeleton: contracts types, the pure +domain layer, domain ports, the six state components from section 8.10, and the +injectable composition root. This front-loads parts of M2 (domain) into the M1 +plan file; the agent loop itself (section 9) is **not** included. + +## Acceptance criteria +- [x] Domain types and pure functions from section 8 exist: `transition`, `PolicyGate.decide`, `effectiveRisk`, `RunCapabilities.narrow`, `Budget` +- [x] Ports from section 8.5 exist as `abstract interface class`: `LlmPort`, `JournalPort`, `MemoryPort`, `ToolPort` +- [x] Contracts: `Risk`, `ToolSpec` (interim hand-written form), `AgentEvent` sealed hierarchy, error codes, `ExtensionManifest` +- [x] State components from section 8.10; `RunBloc` applies the pure `transition` function only (ordering model changed — see T21) +- [x] injectable composition root with environments; `PolicyGate` registered with fail-closed defaults +- [x] `melos run ci` green; tests cover the transition matrix (including invalid transitions), all `PolicyGate.decide` branches, `narrow`, `effectiveRisk`, event JSON round-trip, and a `bloc_test` suite (closes T14) + +## Steps +- [x] Root `analysis_options.yaml`: add the section 7.12 lint set +- [x] `packages/contracts`: `risk.dart`, `tool_spec.dart`, `agent_event.dart`, `error_codes.dart`, `manifest.dart`, barrel +- [x] `runtime/core` domain: `run_state`, `policy`, `budget`, `invocation`, `ids`, `messages`, `cancel_token`, `memory`, `context`, `trigger` +- [x] `runtime/core` ports: `llm_port`, `journal_port`, `memory_port`, `tool_port` +- [x] `runtime/core` application: `RunBloc`, `ApprovalsBloc`, `ExtensionsBloc`, `SchedulerBloc`, `TriggersCubit`, `ChannelsCubit`, `StatePublisher`, `bloc_defaults` +- [x] `runtime/core` DI: `CoreModule` (fail-closed `PolicyGate` default), `injection.dart` with `@InjectableInit`, environments `dev`/`prod`/`test` +- [x] Tests: transition matrix, policy branches, capabilities, budget, contracts round-trip, `RunBloc` lifecycle, component suites, DI suite +- [x] `melos run gen` (injectable config), then `melos run ci` + +## Open checks [VERIFY] +- [ ] T2: schemantic annotations — `ToolSpec`, `ExtensionManifest`, and the section 10.3 DTOs are hand-written until the spike passes; golden tests then freeze the JSON shapes +- [x] T14: `bloc_test` 10 runs against bloc 9.2 — closed by the `RunBloc` suite +- [ ] `AgentEvent` subtype fields beyond `RunStarted` (shown in the spec) are minimal skeletons; golden tests pin them + +## Outcome + +Implemented on 2026-10-19. `melos run ci` green; 144 tests in +`alteri_one_core` plus the contracts suite. + +Toolchain findings recorded in `docs/spec/open-questions.md`: + +- Q5: `public_member_api_docs` cannot see primary-constructor parameter docs + on SDK 3.13.5 — lint omitted from the shared set, docs stay mandatory by + convention. +- Q6: the compiler's verified `new` constructor forms differ from the + section 7.3 examples (space, not dot; type-less named factories; no named + `new` factories). Code follows the compiler. +- T21: bloc 9 runs each `on()` as a separate subscription, so per-handler + `sequential()` does not order events across types. The composition root + sets `Bloc.transformer = sequential()` once (`applyBlocDefaults()`), and + `RunBloc`/`ApprovalsBloc`/`ExtensionsBloc` use a single handler with an + exhaustive `switch` over their sealed event types. Section 8.10 rule 4 + should be rewritten; an ADR can record the pattern. + +Postponed: the agent loop (section 9), journal infrastructure, and the +`StatePublisher` wiring (M2); `tools/arch_check` and the M0 spikes. diff --git a/docs/spec/open-questions.md b/docs/spec/open-questions.md index 9c0e4d6..bb9ffbc 100644 --- a/docs/spec/open-questions.md +++ b/docs/spec/open-questions.md @@ -11,16 +11,19 @@ Added during the M0 scaffold (2026-10-17). Until answered, the scaffold takes th | Q1 | schemantic line: the spec pins 0.2.x ("do not use 1.0.0-rc.1 until verified"), but `genkit 1.0.0` declares `schemantic: ^1.0.0`, so 0.2.x cannot coexist with genkit in one workspace, and schemantic 1.0.0 is now a stable release. Confirm the move to the 1.0.0 line and update section 4. | pinned `schemantic: ^1.0.0` everywhere | | Q2 | Melos line: the spec pins Melos 7 everywhere; Melos 8.x is the current pub.dev release. Migrate to 8 and update sections 4 and 5.2? [VERIFY T12] covers script syntax on 7.x. | pinned `melos: ^7.0.0` (resolves 7.8.2) | | Q3 | `tools/docs_check` is required by `docs/spec/index.md` ("Rules for maintaining documents") but is missing from the repository layout in `docs/spec/architecture.md`, section 5. Add it to section 5? | scaffolded `tools/docs_check` | -| Q4 | Repository URL: the spec gives the repository name `alteri-one` but not the GitHub owner. All pubspecs and docs use `https://github.com/alteri-one/alteri-one` as a placeholder. | placeholder URL | +| Q4 | ~~Repository URL: the spec gives the repository name `alteri-one` but not the GitHub owner.~~ Resolved 2026-10-17: the owner is `dip-develop`; all pubspecs and docs use `https://github.com/dip-develop/alteri-one`. | resolved | +| Q5 | `public_member_api_docs` cannot coexist with the primary-constructor style mandated by section 7.2: the Dart 3.13.5 analyzer does not attach `///` comments to primary-constructor parameters (the style used by every example in `domain.md` section 8). Re-enable the lint when the analyzer supports parameter docs. Until then, parameter dartdoc stays mandatory by convention; `packages/AGENTS.md` ("public classes and methods have English dartdoc") is enforced in review, not by the analyzer. | lint omitted from the shared set; docs kept by convention | +| Q6 | Section 7.3 shows named `new` constructors with a dot (`new origin()`); the Dart 3.13.5 compiler rejects the dot and requires a space (`new origin() : x = 0, y = 0;`), named factories are written without the class name (`factory standard() { ... }`), and named `new` factories are illegal. Update section 7.3 with the compiler-verified forms. | code follows the compiler forms probed on 3.13.5 | Previously none were open. Decisions are recorded in ADR-0005, ADR-0006, and ADR-0009. ## 25.2 Technical checks [VERIFY] -T1 is closed (ADR-0007). +T1 is closed (ADR-0007). T14 is closed: the `RunBloc` suite in `alteri_one_core` runs under `bloc_test` 10 with bloc 9.2. | ID | What to verify | When | What to do on failure | |---|---|---|---| +| T21 | bloc 9 applies each `on()` transformer as a separate subscription: events of different types are never ordered against each other, so per-handler `sequential()` does not make a multi-handler bloc sequential. `Bloc.transformer` (static, set once in the composition root via `applyBlocDefaults()`) is the workspace default; ordering across types additionally requires one handler per bloc (an exhaustive `switch` over the sealed event type). Section 8.10 rule 4 should be rewritten accordingly. | M1 (done, record as ADR input) | — | | T2 | schemantic 0.2.x annotations and compatibility with SDK 3.13 | M0 | write the JSON Schema by hand for the problematic type | | T3 | `returnToolRequests` together with `genkit_openai` and `llama-server` | M3 | a custom HTTP client behind the same port | | T4 | `genkit_openai` behavior with invalid tool arguments | M3 | a custom client that handles `malformed` | diff --git a/packages/contracts/lib/alteri_one_contracts.dart b/packages/contracts/lib/alteri_one_contracts.dart index 45c4b80..169818f 100644 --- a/packages/contracts/lib/alteri_one_contracts.dart +++ b/packages/contracts/lib/alteri_one_contracts.dart @@ -1,12 +1,16 @@ // Copyright 2026 The AlteriOne Authors. // SPDX-License-Identifier: Apache-2.0 -/// Protocol, DTOs, events, `ToolSpec`, and the extension manifest for AlteriOne. +/// Serializable contracts for AlteriOne: protocol events, tool specifications, +/// extension manifests, and error codes. /// -/// JSON shapes defined here are additive-only: removing or redefining a field is a -/// breaking (major) change. See `docs/spec/contracts.md` in the repository. -/// -/// Part of the [AlteriOne](https://alteri.one) monorepo. -/// Status: scaffold only — implementation follows the milestones in -/// `docs/spec/roadmap.md`. +/// JSON shapes defined here are additive-only: removing or redefining a field +/// is a breaking (major) change. See `docs/spec/contracts.md` in the +/// repository. library; + +export 'src/agent_event.dart'; +export 'src/error_codes.dart'; +export 'src/manifest.dart'; +export 'src/risk.dart'; +export 'src/tool_spec.dart'; diff --git a/packages/contracts/lib/src/agent_event.dart b/packages/contracts/lib/src/agent_event.dart new file mode 100644 index 0000000..dc8cb8d --- /dev/null +++ b/packages/contracts/lib/src/agent_event.dart @@ -0,0 +1,264 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Protocol events published by the daemon over SSE and to extensions. +/// +/// Hand-written, not generated: schemantic does not describe sealed +/// hierarchies with a `type` discriminator (docs/spec/contracts.md, +/// section 10.4). Clients must skip unknown types through [UnknownEvent]. +sealed class AgentEvent { + /// Creates an event with its timestamp and, for durable events, its `seq`. + const new({required this.at, this.seq}); + + /// When the event happened. + final DateTime at; + + /// Journal sequence number. Null for ephemeral events such as `run.delta`, + /// which are not replayed. + final int? seq; + + /// Decodes an event from JSON. An unknown `type` becomes [UnknownEvent], so + /// a newer server does not break an older client. + factory fromJson(Map json) => switch (json['type']) { + 'run.started' => RunStarted.fromJson(json), + 'run.delta' => RunDelta.fromJson(json), + 'run.finished' => RunFinished.fromJson(json), + 'approval.requested' => ApprovalRequested.fromJson(json), + 'approval.resolved' => ApprovalResolved.fromJson(json), + 'extension.state' => ExtensionStateChanged.fromJson(json), + 'server.shutdown' => ServerShutdown( + at: DateTime.parse(json['at']! as String), + ), + _ => UnknownEvent.fromJson(json), + }; +} + +/// A run moved out of `queued`. +final class RunStarted extends AgentEvent { + /// Creates the event. + const new({ + required super.at, + super.seq, + required this.runId, + required this.profile, + }); + + /// The run identifier. + final String runId; + + /// The profile the run executes. + final String profile; + + /// Decodes the event from JSON. + factory fromJson(Map j) => RunStarted( + at: DateTime.parse(j['at']! as String), + seq: j['seq'] as int?, + runId: j['run_id']! as String, + profile: j['profile']! as String, + ); + + /// Encodes the event; mirrors [RunStarted.fromJson]. + Map toJson() => { + 'type': 'run.started', + 'at': at.toIso8601String(), + 'seq': ?seq, + 'run_id': runId, + 'profile': profile, + }; +} + +/// A fragment of a streamed reply. Ephemeral: `seq` is null, no replay. +final class RunDelta extends AgentEvent { + /// Creates the event. + const new({required super.at, required this.runId, required this.text}); + + /// The run the fragment belongs to. + final String runId; + + /// The text fragment. + final String text; + + /// Decodes the event from JSON. + factory fromJson(Map j) => RunDelta( + at: DateTime.parse(j['at']! as String), + runId: j['run_id']! as String, + text: j['text']! as String, + ); + + /// Encodes the event; mirrors [RunDelta.fromJson]. + Map toJson() => { + 'type': 'run.delta', + 'at': at.toIso8601String(), + 'run_id': runId, + 'text': text, + }; +} + +/// A run reached a terminal state. +final class RunFinished extends AgentEvent { + /// Creates the event. + const new({ + required super.at, + super.seq, + required this.runId, + required this.state, + this.result, + }); + + /// The run identifier. + final String runId; + + /// Terminal state name, for example `completed` or `failed`. + final String state; + + /// Final reply text when the run completed. + final String? result; + + /// Decodes the event from JSON. + factory fromJson(Map j) => RunFinished( + at: DateTime.parse(j['at']! as String), + seq: j['seq'] as int?, + runId: j['run_id']! as String, + state: j['state']! as String, + result: j['result'] as String?, + ); + + /// Encodes the event; mirrors [RunFinished.fromJson]. + Map toJson() => { + 'type': 'run.finished', + 'at': at.toIso8601String(), + 'seq': ?seq, + 'run_id': runId, + 'state': state, + 'result': ?result, + }; +} + +/// A tool call is waiting for the owner's decision. +final class ApprovalRequested extends AgentEvent { + /// Creates the event. + const new({ + required super.at, + super.seq, + required this.approvalId, + required this.runId, + required this.tool, + }); + + /// The approval identifier. + final String approvalId; + + /// The run that requested the approval. + final String runId; + + /// The LLM name of the tool that wants to run. + final String tool; + + /// Decodes the event from JSON. + factory fromJson(Map j) => ApprovalRequested( + at: DateTime.parse(j['at']! as String), + seq: j['seq'] as int?, + approvalId: j['approval_id']! as String, + runId: j['run_id']! as String, + tool: j['tool']! as String, + ); + + /// Encodes the event; mirrors [ApprovalRequested.fromJson]. + Map toJson() => { + 'type': 'approval.requested', + 'at': at.toIso8601String(), + 'seq': ?seq, + 'approval_id': approvalId, + 'run_id': runId, + 'tool': tool, + }; +} + +/// An approval was granted or denied; the first decision wins. +final class ApprovalResolved extends AgentEvent { + /// Creates the event. + const new({ + required super.at, + super.seq, + required this.approvalId, + required this.granted, + }); + + /// The approval identifier. + final String approvalId; + + /// Whether the action may run. + final bool granted; + + /// Decodes the event from JSON. + factory fromJson(Map j) => ApprovalResolved( + at: DateTime.parse(j['at']! as String), + seq: j['seq'] as int?, + approvalId: j['approval_id']! as String, + granted: j['granted']! as bool, + ); + + /// Encodes the event; mirrors [ApprovalResolved.fromJson]. + Map toJson() => { + 'type': 'approval.resolved', + 'at': at.toIso8601String(), + 'seq': ?seq, + 'approval_id': approvalId, + 'granted': granted, + }; +} + +/// A loaded extension changed its lifecycle state. +final class ExtensionStateChanged extends AgentEvent { + /// Creates the event. + const new({ + required super.at, + super.seq, + required this.extensionId, + required this.state, + }); + + /// The extension identifier from its manifest. + final String extensionId; + + /// Lifecycle state: `loaded`, `draining`, `crashed`, or `stopped`. + final String state; + + /// Decodes the event from JSON. + factory fromJson(Map j) => ExtensionStateChanged( + at: DateTime.parse(j['at']! as String), + seq: j['seq'] as int?, + extensionId: j['extension_id']! as String, + state: j['state']! as String, + ); + + /// Encodes the event; mirrors [ExtensionStateChanged.fromJson]. + Map toJson() => { + 'type': 'extension.state', + 'at': at.toIso8601String(), + 'seq': ?seq, + 'extension_id': extensionId, + 'state': state, + }; +} + +/// The daemon is shutting down; subscribers should reconnect with backoff. +final class ServerShutdown extends AgentEvent { + /// Creates the event. + const new({required super.at}); +} + +/// An event type this client does not know. Skipped, never an error. +final class UnknownEvent extends AgentEvent { + /// Creates the event. + const new({required super.at, required this.type}); + + /// The unrecognized `type` string. + final String type; + + /// Decodes the event, preserving the original type string. + factory fromJson(Map j) => UnknownEvent( + at: DateTime.parse(j['at']! as String), + type: '${j['type']}', + ); +} diff --git a/packages/contracts/lib/src/error_codes.dart b/packages/contracts/lib/src/error_codes.dart new file mode 100644 index 0000000..d75beea --- /dev/null +++ b/packages/contracts/lib/src/error_codes.dart @@ -0,0 +1,44 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// API error codes of the public protocol (docs/spec/contracts.md, 10.3). +/// +/// A client must treat an unknown code as a generic error. Messages never +/// contain secrets or fragments of prompts. +abstract final class ApiErrorCode { + /// Run not found (404). + static const runNotFound = 'run_not_found'; + + /// A decision has already been made (409). + static const approvalConflict = 'approval_conflict'; + + /// Insufficient scope (403). + static const forbidden = 'forbidden'; + + /// Missing or invalid token (401). + static const unauthorized = 'unauthorized'; + + /// The daemon is stopping (503). + static const shuttingDown = 'shutting_down'; + + /// Validation failed (400). + static const invalidRequest = 'invalid_request'; + + /// The extension is unloaded or has crashed (503). + static const extensionUnavailable = 'extension_unavailable'; + + /// Budget limit reached (429). + static const budgetExceeded = 'budget_exceeded'; +} + +/// A structured error of the public API. +final class ApiError( + /// One of [ApiErrorCode]. + final String code, + + /// Human-readable message without secrets. + final String message, + + /// Optional structured details. + final Map? details, +); diff --git a/packages/contracts/lib/src/manifest.dart b/packages/contracts/lib/src/manifest.dart new file mode 100644 index 0000000..103fff0 --- /dev/null +++ b/packages/contracts/lib/src/manifest.dart @@ -0,0 +1,57 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// What an extension provides (docs/spec/contracts.md, section 10.5). +final class ExtensionProvides( + /// Tool names without the `{extId}__` prefix. + final List tools, + + /// Trigger ids the extension registers. + final List triggers, + + /// Channel ids the extension registers. + final List channels, + + /// Context provider ids the extension registers. + final List context, + + /// Whether the extension is a memory backend. + final bool memoryBackend, + + /// Skill names the extension ships. + final List skills, +); + +/// Extension manifest: identity, contents, permissions, and signature. +/// +/// Interim hand-written form; validated against a schemantic-generated JSON +/// Schema after spike T2. The signature is computed over the canonical JSON +/// produced from the parsed YAML. +final class ExtensionManifest( + /// Extension identifier, for example `weather`. + final String id, + + /// Extension version. + final String version, + + /// `alteri_one_contracts` version the extension was built against. + final String contract, + + /// Exact Dart SDK version used to build the snapshot. + final String sdk, + + /// Target platform, for example `linux-x64`. + final String target, + + /// What the extension registers. + final ExtensionProvides provides, + + /// Granted permissions, for example `net:api.open-meteo.com`. + final List permissions, + + /// Hash of the `.aot` snapshot. + final String sha256, + + /// Ed25519 signature over the canonical manifest JSON. + final String signature, +); diff --git a/packages/contracts/lib/src/risk.dart b/packages/contracts/lib/src/risk.dart new file mode 100644 index 0000000..068bc89 --- /dev/null +++ b/packages/contracts/lib/src/risk.dart @@ -0,0 +1,21 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Risk classes of tools and actions. +/// +/// Shared with the core domain (docs/spec/domain.md, section 8.2). Together +/// with [ToolSpec] this is the public language of extensions and is used +/// directly, without DTO mapping (docs/spec/contracts.md, section 10.1). +enum Risk { + /// Reads data, no side effects. + read, + + /// Writes to the owner's data: files, memory, configuration. + write, + + /// Talks to the network or another process. + external, + + /// Deletes data or cannot be undone. + destructive, +} diff --git a/packages/contracts/lib/src/tool_spec.dart b/packages/contracts/lib/src/tool_spec.dart new file mode 100644 index 0000000..5df0bf7 --- /dev/null +++ b/packages/contracts/lib/src/tool_spec.dart @@ -0,0 +1,62 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'risk.dart'; + +/// Specification of a tool: the public language shared by the core, providers, +/// and extensions (docs/spec/contracts.md, section 10.3). +/// +/// Interim hand-written form: once spike T2 verifies the schemantic +/// annotations on SDK 3.13, this class is regenerated from an `abstract class +/// $ToolSpec` and the JSON shape is frozen by golden tests (M1). +final class ToolSpec( + /// Canonical tool name, for example `forecast`. + final String name, + + /// Name the model sees: `{extId}__{name}` (docs/spec/extensions.md). + final String llmName, + + /// One-line description shown to the model. + final String description, + + /// Declared risk. Configuration may raise it, never lower it. + final Risk risk, + + /// Execution timeout in milliseconds. TODO(m1): pin the default in the + /// configuration defaults (section 14); 30 s is the interim value. + final int timeoutMs, + + /// Tool output is truncated to this many bytes before entering the model + /// context. TODO(m1): pin the default; 16 KiB is the interim value. + final int maxOutputBytes, + + /// Whether the output must be treated as untrusted data. Default `true`; + /// `false` only for computations without external data (section 8.2). + final bool untrustedOutput, + + /// JSON Schema of the input as a raw map (no fixed schema, section 10.2). + final Map? inputSchema, +) { + /// Default timeout and output limits used when a spec does not set them. + static const defaultTimeoutMs = 30_000; + static const defaultMaxOutputBytes = 16_384; + + /// Creates a spec with the default timeout and output limit. + factory defaults({ + required String name, + required String llmName, + required String description, + required Risk risk, + bool untrustedOutput = true, + Map? inputSchema, + }) => ToolSpec( + name, + llmName, + description, + risk, + defaultTimeoutMs, + defaultMaxOutputBytes, + untrustedOutput, + inputSchema, + ); +} diff --git a/packages/contracts/test/agent_event_test.dart b/packages/contracts/test/agent_event_test.dart new file mode 100644 index 0000000..62ef133 --- /dev/null +++ b/packages/contracts/test/agent_event_test.dart @@ -0,0 +1,124 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; +import 'package:test/test.dart'; + +void main() { + group('AgentEvent.fromJson', () { + test('decodes run.started with all fields', () { + final event = AgentEvent.fromJson(const { + 'type': 'run.started', + 'at': '2026-10-18T10:00:00Z', + 'seq': 42, + 'run_id': 'r-1', + 'profile': 'assistant', + }); + final started = event as RunStarted; + expect(started.runId, 'r-1'); + expect(started.profile, 'assistant'); + expect(started.seq, 42); + }); + + test('run.started round-trips through toJson', () { + final original = RunStarted( + at: DateTime.parse('2026-10-18T10:00:00Z'), + seq: 7, + runId: 'r-1', + profile: 'assistant', + ); + final decoded = AgentEvent.fromJson(original.toJson()); + expect(decoded, isA()); + final started = decoded as RunStarted; + expect(started.runId, 'r-1'); + expect(started.profile, 'assistant'); + expect(started.seq, 7); + }); + + test('run.delta is ephemeral and carries no seq', () { + final decoded = AgentEvent.fromJson( + RunDelta( + at: DateTime.parse('2026-10-18T10:00:00Z'), + runId: 'r-1', + text: 'Hel', + ).toJson(), + ); + final delta = decoded as RunDelta; + expect(delta.seq, isNull); + expect(delta.text, 'Hel'); + // toJson omits the null seq entirely. + expect( + RunDelta( + at: DateTime.parse('2026-10-18T10:00:00Z'), + runId: 'r-1', + text: '', + ).toJson().containsKey('seq'), + isFalse, + ); + }); + + test('an unknown type becomes UnknownEvent, not an error', () { + final event = AgentEvent.fromJson(const { + 'type': 'run.progress.v2', + 'at': '2026-10-18T10:00:00Z', + }); + final unknown = event as UnknownEvent; + expect(unknown.type, 'run.progress.v2'); + }); + + test('approval and extension events round-trip', () { + final resolved = AgentEvent.fromJson( + ApprovalResolved( + at: DateTime.parse('2026-10-18T10:00:00Z'), + seq: 9, + approvalId: 'a-1', + granted: false, + ).toJson(), + ); + expect((resolved as ApprovalResolved).granted, isFalse); + + final changed = AgentEvent.fromJson( + ExtensionStateChanged( + at: DateTime.parse('2026-10-18T10:00:00Z'), + seq: 10, + extensionId: 'mcp', + state: 'draining', + ).toJson(), + ); + expect((changed as ExtensionStateChanged).state, 'draining'); + }); + }); + + group('ToolSpec', () { + test('defaults: 30 s timeout, 16 KiB output, untrusted output', () { + final spec = ToolSpec.defaults( + name: 'forecast', + llmName: 'weather__forecast', + description: 'Weather forecast', + risk: Risk.external, + ); + expect(spec.timeoutMs, ToolSpec.defaultTimeoutMs); + expect(spec.maxOutputBytes, ToolSpec.defaultMaxOutputBytes); + expect(spec.untrustedOutput, isTrue); + }); + + test('untrustedOutput can be opted out for pure computations', () { + final spec = ToolSpec.defaults( + name: 'sqrt', + llmName: 'calc__sqrt', + description: 'Square root', + risk: Risk.read, + untrustedOutput: false, + ); + expect(spec.untrustedOutput, isFalse); + }); + }); + + group('error codes', () { + test('carry the protocol strings', () { + expect(ApiErrorCode.runNotFound, 'run_not_found'); + expect(ApiErrorCode.approvalConflict, 'approval_conflict'); + expect(ApiErrorCode.budgetExceeded, 'budget_exceeded'); + }); + }); +} diff --git a/runtime/core/lib/alteri_one_core.dart b/runtime/core/lib/alteri_one_core.dart index 3f4804e..9583b08 100644 --- a/runtime/core/lib/alteri_one_core.dart +++ b/runtime/core/lib/alteri_one_core.dart @@ -1,13 +1,36 @@ // Copyright 2026 The AlteriOne Authors. // SPDX-License-Identifier: Apache-2.0 -/// The AlteriOne core: domain, use cases, run state, and the HTTP server. +/// The AlteriOne core: domain, ports, application state, and composition. /// -/// One runtime, three run modes (`chat`, `task`, `resident`). Only `PolicyGate` -/// decides whether a tool runs; the journal is written before any side effect; run -/// state is a projection of the journal. This package is not published. -/// -/// Part of the [AlteriOne](https://alteri.one) monorepo. -/// Status: scaffold only — implementation follows the milestones in -/// `docs/spec/roadmap.md`. +/// One runtime, three run modes (`chat`, `task`, `resident`). Only +/// `PolicyGate` decides whether a tool runs; the journal is written before +/// any side effect; run state is a projection of the journal. This package is +/// not published. library; + +// Application state (docs/spec/domain.md, section 8.10) and +// composition; then the pure domain (sections 8.1–8.9). +export 'src/application/approvals_bloc.dart'; +export 'src/application/bloc_defaults.dart'; +export 'src/application/channels_cubit.dart'; +export 'src/application/extensions_bloc.dart'; +export 'src/application/run_bloc.dart'; +export 'src/application/scheduler_bloc.dart'; +export 'src/application/state_publisher.dart'; +export 'src/application/triggers_cubit.dart'; +export 'src/di/injection.dart'; +export 'src/domain/budget.dart'; +export 'src/domain/cancel_token.dart'; +export 'src/domain/context.dart'; +export 'src/domain/ids.dart'; +export 'src/domain/invocation.dart'; +export 'src/domain/memory.dart'; +export 'src/domain/messages.dart'; +export 'src/domain/policy.dart'; +export 'src/domain/ports/journal_port.dart'; +export 'src/domain/ports/llm_port.dart'; +export 'src/domain/ports/memory_port.dart'; +export 'src/domain/ports/tool_port.dart'; +export 'src/domain/run_state.dart'; +export 'src/domain/trigger.dart'; diff --git a/runtime/core/lib/src/application/approvals_bloc.dart b/runtime/core/lib/src/application/approvals_bloc.dart new file mode 100644 index 0000000..da93ce0 --- /dev/null +++ b/runtime/core/lib/src/application/approvals_bloc.dart @@ -0,0 +1,95 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:injectable/injectable.dart'; + +import '../domain/ids.dart'; + +/// An approval waiting for the owner's decision. +final class PendingApproval( + /// Approval identifier. + final ApprovalId id, + + /// The run that waits. + final RunId runId, + + /// LLM name of the tool that wants to run. + final String tool, + + /// When the approval expires. No answer before the deadline means refusal. + final DateTime deadline, +); + +/// State of [ApprovalsBloc]: the pending approvals. +final class ApprovalsState( + /// Pending approvals by identifier. + final Map pending, +) { + /// The state with no pending approvals. + factory empty() => ApprovalsState(const {}); + + /// Copies the state with a replaced map. + ApprovalsState withPending(Map value) => + ApprovalsState(value); +} + +/// Events of [ApprovalsBloc]. +sealed class ApprovalsEvent { + const new(); +} + +/// A tool call asked for the owner's decision. +final class ApprovalRequestedEvent extends ApprovalsEvent { + /// Creates the event. + const new(this.approval); + + /// The approval that entered the queue. + final PendingApproval approval; +} + +/// The owner decided. The first decision for an approval wins. +final class ApprovalDecidedEvent extends ApprovalsEvent { + /// Creates the event. + const new(this.id, {required this.granted}); + + /// The approval that was decided. + final ApprovalId id; + + /// Whether the action may run. + final bool granted; +} + +/// Pending approvals; the first decision wins +/// (docs/spec/domain.md, section 8.10). +/// +/// Skeleton: the M2 application service writes the request and the decision +/// to the journal before the effects run. +/// +/// Like [RunBloc], all events share one handler so that decisions are +/// ordered after their requests regardless of event type. +@lazySingleton +class ApprovalsBloc extends Bloc { + /// Creates the bloc with an empty queue. + new() : super(ApprovalsState.empty()) { + on(_onEvent); + } + + void _onEvent(ApprovalsEvent event, Emitter emit) { + switch (event) { + case ApprovalRequestedEvent(:final approval): + // The first decision wins: a second request for an id already in the + // map is ignored. + if (state.pending.containsKey(approval.id)) { + return; + } + emit(state.withPending({...state.pending, approval.id: approval})); + case ApprovalDecidedEvent(:final id): + // A decision for an already resolved approval changes nothing. + if (!state.pending.containsKey(id)) { + return; + } + emit(state.withPending({...state.pending}..remove(id))); + } + } +} diff --git a/runtime/core/lib/src/application/bloc_defaults.dart b/runtime/core/lib/src/application/bloc_defaults.dart new file mode 100644 index 0000000..e93f34c --- /dev/null +++ b/runtime/core/lib/src/application/bloc_defaults.dart @@ -0,0 +1,21 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; + +/// Applies the core's default bloc configuration +/// (docs/spec/domain.md, section 8.10, rule 4). +/// +/// bloc 9 has no per-instance default transformer, so the default is set +/// statically, exactly once, in the composition root: `sequential()` for +/// every event stream. Components that need different semantics pass a +/// per-handler transformer, which takes precedence — for example +/// `SchedulerBloc` uses `droppable()` for ticks. +/// +/// `concurrent()` is not used in the core without a justification. A bloc +/// created before this call (only possible in tests) keeps the bloc default +/// transformer and must order its events per handler. +void applyBlocDefaults() { + Bloc.transformer = sequential(); +} diff --git a/runtime/core/lib/src/application/channels_cubit.dart b/runtime/core/lib/src/application/channels_cubit.dart new file mode 100644 index 0000000..0927cd8 --- /dev/null +++ b/runtime/core/lib/src/application/channels_cubit.dart @@ -0,0 +1,56 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:injectable/injectable.dart'; + +/// Availability of a channel. +enum ChannelStatus { + /// Connected: `askApproval` can reach the owner. + connected, + + /// Unavailable: `ask` decisions degrade to `deny` (fail closed). + unavailable, +} + +/// State of [ChannelsCubit]. +final class ChannelsState( + /// Channel status by channel id (for example `cli` or `app`). + final Map channels, +) { + /// The state with no channels. + factory empty() => ChannelsState(const {}); + + /// Copies the state with a replaced map. + ChannelsState withChannels(Map value) => + ChannelsState(value); + + /// Whether at least one channel can reach the owner; feeds + /// `PolicyGate.hasInteractiveChannel`. + bool get hasInteractive => channels.values.contains(ChannelStatus.connected); +} + +/// Channel status: connected or unavailable (docs/spec/domain.md, 8.10). +/// +/// Skeleton: pairing and SSE transport land in M4. +@lazySingleton +class ChannelsCubit extends Cubit { + /// Creates the cubit with no channels. + new() : super(ChannelsState.empty()); + + /// Registers a channel in its initial status. + void register(String id, ChannelStatus status) { + if (state.channels.containsKey(id)) { + return; + } + emit(state.withChannels({...state.channels, id: status})); + } + + /// Updates the status of a registered channel. + void setStatus(String id, ChannelStatus status) { + if (!state.channels.containsKey(id)) { + return; + } + emit(state.withChannels({...state.channels, id: status})); + } +} diff --git a/runtime/core/lib/src/application/extensions_bloc.dart b/runtime/core/lib/src/application/extensions_bloc.dart new file mode 100644 index 0000000..9a7a0b0 --- /dev/null +++ b/runtime/core/lib/src/application/extensions_bloc.dart @@ -0,0 +1,87 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:injectable/injectable.dart'; + +/// Lifecycle states of an extension (docs/spec/domain.md, section 8.10). +enum ExtensionLifecycle { + /// Registered and serving calls. + loaded, + + /// Stopping: in-flight calls finish, new calls are refused. + draining, + + /// Died unexpectedly. + crashed, + + /// Stopped cleanly. + stopped, +} + +/// State of [ExtensionsBloc]: the extension registry. +final class ExtensionsState( + /// Extension lifecycle by extension id. + final Map registry, +) { + /// The state with no extensions. + factory empty() => ExtensionsState(const {}); + + /// Copies the state with a replaced map. + ExtensionsState withRegistry(Map value) => + ExtensionsState(value); +} + +/// Events of [ExtensionsBloc]. +sealed class ExtensionsEvent { + const new(); +} + +/// An extension entered the registry. +final class ExtensionRegisteredEvent extends ExtensionsEvent { + /// Creates the event. + const new(this.extensionId, this.lifecycle); + + /// The extension identifier from its manifest. + final String extensionId; + + /// Its current lifecycle state. + final ExtensionLifecycle lifecycle; +} + +/// An extension changed its lifecycle state. +final class ExtensionLifecycleChangedEvent extends ExtensionsEvent { + /// Creates the event. + const new(this.extensionId, this.lifecycle); + + /// The extension identifier from its manifest. + final String extensionId; + + /// The new lifecycle state. + final ExtensionLifecycle lifecycle; +} + +/// The extension registry: loaded, draining, crashed, stopped +/// (docs/spec/domain.md, section 8.10). +/// +/// Skeleton: loading and draining effects land in M5. Like [RunBloc], all +/// events share one handler so registry changes stay ordered. +@lazySingleton +class ExtensionsBloc extends Bloc { + /// Creates the bloc with an empty registry. + new() : super(ExtensionsState.empty()) { + on(_onEvent); + } + + void _onEvent(ExtensionsEvent event, Emitter emit) { + switch (event) { + case ExtensionRegisteredEvent(:final extensionId, :final lifecycle): + emit(state.withRegistry({...state.registry, extensionId: lifecycle})); + case ExtensionLifecycleChangedEvent(:final extensionId, :final lifecycle): + if (!state.registry.containsKey(extensionId)) { + return; + } + emit(state.withRegistry({...state.registry, extensionId: lifecycle})); + } + } +} diff --git a/runtime/core/lib/src/application/run_bloc.dart b/runtime/core/lib/src/application/run_bloc.dart new file mode 100644 index 0000000..26e1bb2 --- /dev/null +++ b/runtime/core/lib/src/application/run_bloc.dart @@ -0,0 +1,202 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; + +import '../domain/ids.dart'; +import '../domain/run_state.dart'; + +/// State of [RunBloc] (docs/spec/domain.md, section 8.10). +final class RunBlocState { + /// Creates the state. + const new({ + required this.runId, + required this.runState, + required this.steps, + this.pendingApproval, + }); + + /// The run this bloc belongs to. + final RunId runId; + + /// Current domain state of the run. + final RunState runState; + + /// Agent-loop steps completed so far. + final int steps; + + /// The approval the run is waiting for, when blocked. + final ApprovalId? pendingApproval; + + /// Copies the state with the given fields replaced. + RunBlocState copyWith({ + RunState? runState, + int? steps, + ApprovalId? pendingApproval, + bool clearPendingApproval = false, + }) => RunBlocState( + runId: runId, + runState: runState ?? this.runState, + steps: steps ?? this.steps, + pendingApproval: clearPendingApproval + ? null + : pendingApproval ?? this.pendingApproval, + ); + + @override + bool operator ==(Object other) => + other is RunBlocState && + other.runId == runId && + other.runState == runState && + other.steps == steps && + other.pendingApproval == pendingApproval; + + @override + int get hashCode => Object.hash(runId, runState, steps, pendingApproval); +} + +/// Events of [RunBloc]. They map one-to-one onto the domain [RunEvent]s; +/// the bloc applies the pure [transition] function and performs no policy or +/// domain computation itself (docs/spec/domain.md, section 8.10). +sealed class RunBlocEvent { + const new(); +} + +/// The run leaves the queue. +final class RunStartedEvent extends RunBlocEvent { + const new(); +} + +/// A tool call needs an owner decision. +final class RunRequestApprovalEvent extends RunBlocEvent { + /// Creates the event. + const new(this.approvalId); + + /// The approval the run waits for. + final ApprovalId approvalId; +} + +/// The owner allowed the action. +final class RunApprovalGrantedEvent extends RunBlocEvent { + const new(); +} + +/// The owner refused the action; the run continues. +final class RunApprovalDeniedEvent extends RunBlocEvent { + const new(); +} + +/// An agent-loop step finished. +final class RunStepDoneEvent extends RunBlocEvent { + const new(); +} + +/// The run reached its goal. +final class RunCompletedEvent extends RunBlocEvent { + const new(); +} + +/// The run failed. +final class RunFailedEvent extends RunBlocEvent { + const new(); +} + +/// The owner cancelled the run. +final class RunCancelledEvent extends RunBlocEvent { + const new(); +} + +/// A budget limit was reached. +final class RunBudgetExceededEvent extends RunBlocEvent { + const new(); +} + +/// The process died. +final class RunCrashedEvent extends RunBlocEvent { + const new(); +} + +/// The run was restored from the journal. +final class RunRecoveredEvent extends RunBlocEvent { + const new(); +} + +/// The owner paused the run. +final class RunPausedEvent extends RunBlocEvent { + const new(); +} + +/// The owner resumed the run. +final class RunResumedEvent extends RunBlocEvent { + const new(); +} + +/// Long-lived state of a single run (docs/spec/domain.md, section 8.10). +/// +/// One instance per run; created by the run supervisor (M2), not registered +/// in the DI container. The handler only applies the pure [transition] +/// function and emits; journal writes and effects (model call, tool call, +/// channel send) are performed by the application service and land in M2. +/// +/// All events share one handler: bloc delivers each `on` registration as +/// a separate subscription, which never orders events across types. A single +/// subscription plus the default `sequential` transformer +/// (see [applyBlocDefaults]) keeps the run state machine strictly ordered. +class RunBloc extends Bloc { + /// Creates a bloc for [runId] in `queued` state. + new({required RunId runId}) + : super(RunBlocState(runId: runId, runState: RunState.queued, steps: 0)) { + on(_onEvent); + } + + void _onEvent(RunBlocEvent event, Emitter emit) { + switch (event) { + case RunStartedEvent(): + _apply(RunEvent.start, emit); + case RunRequestApprovalEvent(:final approvalId): + emit( + state.copyWith( + runState: transition(state.runState, RunEvent.requestApproval), + pendingApproval: approvalId, + ), + ); + case RunApprovalGrantedEvent(): + _apply(RunEvent.approvalGranted, emit, clearApproval: true); + case RunApprovalDeniedEvent(): + _apply(RunEvent.approvalDenied, emit, clearApproval: true); + case RunStepDoneEvent(): + emit(state.copyWith(steps: state.steps + 1)); + case RunCompletedEvent(): + _apply(RunEvent.complete, emit); + case RunFailedEvent(): + _apply(RunEvent.fail, emit); + case RunCancelledEvent(): + _apply(RunEvent.cancel, emit); + case RunBudgetExceededEvent(): + _apply(RunEvent.exceedBudget, emit); + case RunCrashedEvent(): + _apply(RunEvent.crash, emit); + case RunRecoveredEvent(): + _apply(RunEvent.recover, emit); + case RunPausedEvent(): + _apply(RunEvent.pause, emit); + case RunResumedEvent(): + _apply(RunEvent.resume, emit); + } + } + + void _apply( + RunEvent domainEvent, + Emitter emit, { + bool clearApproval = false, + }) { + // An invalid transition throws and is reported through the bloc + // observer; it is never silently ignored (docs/spec/domain.md, 8.1). + emit( + state.copyWith( + runState: transition(state.runState, domainEvent), + clearPendingApproval: clearApproval, + ), + ); + } +} diff --git a/runtime/core/lib/src/application/scheduler_bloc.dart b/runtime/core/lib/src/application/scheduler_bloc.dart new file mode 100644 index 0000000..1fbadd6 --- /dev/null +++ b/runtime/core/lib/src/application/scheduler_bloc.dart @@ -0,0 +1,45 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:injectable/injectable.dart'; + +/// State of [SchedulerBloc]. +final class SchedulerState( + /// When the last processed tick fired; null before the first one. + final DateTime? lastTick, +) { + /// The state before any tick. + factory initial() => SchedulerState(null); + + /// Copies the state with a replaced `lastTick`. + SchedulerState withLastTick(DateTime value) => SchedulerState(value); +} + +/// A schedule tick fired (docs/spec/domain.md, section 8.10). +final class SchedulerTickEvent { + /// Creates the event. + const new(this.at); + + /// When the tick fired. + final DateTime at; +} + +/// Schedule ticks. The `droppable` transformer skips a tick while the +/// previous one is still being processed (docs/spec/domain.md, 8.10). +/// +/// Skeleton: starting scheduled runs lands in M2; the handler currently only +/// records the tick. +@lazySingleton +class SchedulerBloc extends Bloc { + /// Creates the bloc. + new() : super(SchedulerState.initial()) { + on(_onTick, transformer: droppable()); + } + + // TODO(m2): evaluate schedules and start due runs inside the handler. + void _onTick(SchedulerTickEvent event, Emitter emit) { + emit(state.withLastTick(event.at)); + } +} diff --git a/runtime/core/lib/src/application/state_publisher.dart b/runtime/core/lib/src/application/state_publisher.dart new file mode 100644 index 0000000..8fb73c5 --- /dev/null +++ b/runtime/core/lib/src/application/state_publisher.dart @@ -0,0 +1,47 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; +import 'package:injectable/injectable.dart'; + +/// Topics that extensions and clients may observe +/// (docs/spec/domain.md, section 8.11). +enum StateTopic { + /// Run lifecycle: started, progress, finished. + runs, + + /// Pending approvals and their decisions. + approvals, + + /// Extension registry changes. + extensions, + + /// Trigger enablement and firing. + triggers, + + /// Channel availability. + channels, + + /// Memory entry metadata only, never entry text. + memoryMeta, +} + +/// The single place where bloc state changes become serializable +/// [AgentEvent]s (docs/spec/domain.md, section 8.11). +/// +/// Extensions receive the same events that clients receive over SSE; there is +/// no separate channel for extensions. On subscription the subscriber first +/// receives a snapshot of the topic and then the events. Each subscriber has +/// a bounded queue: `runs.progress` coalesces to the latest value, while +/// `approvals.*` and `runs.finished` guarantee delivery and disconnect the +/// subscriber with `resync_required` on overflow. +/// +/// Skeleton: wiring, snapshots, and queue policies land in M2 (clients) and +/// M5 (extension observation). +@lazySingleton +class StatePublisher { + /// Returns the events of [topic] starting with a snapshot. + /// + /// Not implemented yet. + Stream subscribe(StateTopic topic) => throw UnimplementedError(); +} diff --git a/runtime/core/lib/src/application/triggers_cubit.dart b/runtime/core/lib/src/application/triggers_cubit.dart new file mode 100644 index 0000000..061c4e7 --- /dev/null +++ b/runtime/core/lib/src/application/triggers_cubit.dart @@ -0,0 +1,80 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:bloc/bloc.dart'; +import 'package:injectable/injectable.dart'; + +/// Status of one trigger. +final class TriggerStatus( + /// Whether the trigger may fire. + final bool enabled, + + /// When the trigger last fired; null if it never did. + final DateTime? lastFired, +) { + /// The disabled, never-fired status. + factory initial() => TriggerStatus(false, null); + + /// Copies the status with replaced fields. + TriggerStatus copyWith({bool? enabled, DateTime? lastFired}) => + TriggerStatus(enabled ?? this.enabled, lastFired ?? this.lastFired); +} + +/// State of [TriggersCubit]. +final class TriggersState( + /// Trigger status by trigger id. + final Map triggers, +) { + /// The state with no triggers. + factory empty() => TriggersState(const {}); + + /// Copies the state with a replaced map. + TriggersState withTriggers(Map value) => + TriggersState(value); +} + +/// Enabled flags and last-firing times of triggers +/// (docs/spec/domain.md, section 8.10). +/// +/// Skeleton: rate limiting (`max_per_hour`) and chain depth checks land in M2. +@lazySingleton +class TriggersCubit extends Cubit { + /// Creates the cubit with an empty registry. + new() : super(TriggersState.empty()); + + /// Registers a trigger; new triggers start disabled. + void register(String id) { + if (state.triggers.containsKey(id)) { + return; + } + emit(state.withTriggers({...state.triggers, id: TriggerStatus.initial()})); + } + + /// Enables or disables a registered trigger. + void setEnabled(String id, bool enabled) { + final status = state.triggers[id]; + if (status == null) { + return; + } + emit( + state.withTriggers({ + ...state.triggers, + id: status.copyWith(enabled: enabled), + }), + ); + } + + /// Records that a trigger fired. + void fired(String id, DateTime at) { + final status = state.triggers[id]; + if (status == null || !status.enabled) { + return; + } + emit( + state.withTriggers({ + ...state.triggers, + id: status.copyWith(lastFired: at), + }), + ); + } +} diff --git a/runtime/core/lib/src/di/core_module.dart b/runtime/core/lib/src/di/core_module.dart new file mode 100644 index 0000000..5f02cb8 --- /dev/null +++ b/runtime/core/lib/src/di/core_module.dart @@ -0,0 +1,19 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:injectable/injectable.dart'; + +import '../domain/policy.dart'; + +/// Third-party registrations for the core container +/// (docs/spec/domain.md, section 8.10). +@module +abstract class CoreModule { + /// The single decision point for tool execution. + /// + /// Fail-closed defaults until profile configuration loading lands in M2: + /// no profile rules and no interactive channel mean every tool call is + /// denied. + @lazySingleton + PolicyGate get policyGate => PolicyGate(const {}, false); +} diff --git a/runtime/core/lib/src/di/injection.config.dart b/runtime/core/lib/src/di/injection.config.dart new file mode 100644 index 0000000..67cdc6f --- /dev/null +++ b/runtime/core/lib/src/di/injection.config.dart @@ -0,0 +1,43 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// dart format width=80 + +// ************************************************************************** +// InjectableConfigGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +// coverage:ignore-file + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:alteri_one_core/src/application/approvals_bloc.dart' as _i511; +import 'package:alteri_one_core/src/application/channels_cubit.dart' as _i456; +import 'package:alteri_one_core/src/application/extensions_bloc.dart' as _i603; +import 'package:alteri_one_core/src/application/scheduler_bloc.dart' as _i221; +import 'package:alteri_one_core/src/application/state_publisher.dart' as _i869; +import 'package:alteri_one_core/src/application/triggers_cubit.dart' as _i476; +import 'package:alteri_one_core/src/di/core_module.dart' as _i398; +import 'package:alteri_one_core/src/domain/policy.dart' as _i665; +import 'package:get_it/get_it.dart' as _i174; +import 'package:injectable/injectable.dart' as _i526; + +extension GetItInjectableX on _i174.GetIt { + // initializes the registration of main-scope dependencies inside of GetIt + _i174.GetIt init({ + String? environment, + _i526.EnvironmentFilter? environmentFilter, + }) { + final gh = _i526.GetItHelper(this, environment, environmentFilter); + final coreModule = _$CoreModule(); + gh.lazySingleton<_i511.ApprovalsBloc>(() => _i511.ApprovalsBloc()); + gh.lazySingleton<_i456.ChannelsCubit>(() => _i456.ChannelsCubit()); + gh.lazySingleton<_i603.ExtensionsBloc>(() => _i603.ExtensionsBloc()); + gh.lazySingleton<_i221.SchedulerBloc>(() => _i221.SchedulerBloc()); + gh.lazySingleton<_i869.StatePublisher>(() => _i869.StatePublisher()); + gh.lazySingleton<_i476.TriggersCubit>(() => _i476.TriggersCubit()); + gh.lazySingleton<_i665.PolicyGate>(() => coreModule.policyGate); + return this; + } +} + +class _$CoreModule extends _i398.CoreModule {} diff --git a/runtime/core/lib/src/di/injection.dart b/runtime/core/lib/src/di/injection.dart new file mode 100644 index 0000000..1701a19 --- /dev/null +++ b/runtime/core/lib/src/di/injection.dart @@ -0,0 +1,22 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:get_it/get_it.dart'; +import 'package:injectable/injectable.dart'; + +import '../application/bloc_defaults.dart'; +import 'injection.config.dart'; + +/// The core's container. Ports (`LlmPort`, `JournalPort`, `MemoryPort`) are +/// registered by infrastructure and providers when those land. +final getIt = GetIt.instance; + +/// Initializes the core container for [environment] +/// (`dev`, `prod`, or `test`). +@InjectableInit() +void configureCore({required String environment}) { + // Configured exactly once, before any bloc is created + // (docs/spec/domain.md, section 8.10, rules 3–4). + applyBlocDefaults(); + getIt.init(environment: environment); +} diff --git a/runtime/core/lib/src/domain/budget.dart b/runtime/core/lib/src/domain/budget.dart new file mode 100644 index 0000000..bef5d1e --- /dev/null +++ b/runtime/core/lib/src/domain/budget.dart @@ -0,0 +1,23 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Per-run budget (docs/spec/domain.md, section 8.3). +/// +/// Exceeding any limit moves the run to `budgetExceeded`. A global daily +/// spending limit is configured separately (section 14). +final class Budget( + /// Maximum agent-loop steps. + final int maxSteps, + + /// Maximum tokens across the whole run. + final int maxTokens, + + /// Wall-clock limit. + final Duration maxDuration, + + /// Maximum tokens in one model reply. + final int maxOutputTokens, +) { + /// The default budget: 12 steps, 60 000 tokens, 180 s, 2 048 output tokens. + factory standard() => Budget(12, 60_000, const Duration(seconds: 180), 2_048); +} diff --git a/runtime/core/lib/src/domain/cancel_token.dart b/runtime/core/lib/src/domain/cancel_token.dart new file mode 100644 index 0000000..afee97a --- /dev/null +++ b/runtime/core/lib/src/domain/cancel_token.dart @@ -0,0 +1,30 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Thrown by [CancelToken.throwIfCancelled] and by cancelled operations. +final class RunCancelledException implements Exception { + /// Creates the exception. + const new(); + + @override + String toString() => 'RunCancelledException'; +} + +/// Cooperative cancellation for runs and tool calls +/// (docs/spec/domain.md, section 9). +final class CancelToken() { + bool _cancelled = false; + + /// Whether cancellation was requested. + bool get isCancelled => _cancelled; + + /// Requests cancellation. + void cancel() => _cancelled = true; + + /// Throws [RunCancelledException] if cancellation was requested. + void throwIfCancelled() { + if (_cancelled) { + throw const RunCancelledException(); + } + } +} diff --git a/runtime/core/lib/src/domain/context.dart b/runtime/core/lib/src/domain/context.dart new file mode 100644 index 0000000..bf67052 --- /dev/null +++ b/runtime/core/lib/src/domain/context.dart @@ -0,0 +1,18 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// A fragment of context provided before a run starts +/// (docs/spec/domain.md, section 8.8). +/// +/// The core wraps `untrusted` fragments in a block with a random boundary +/// (section 13); untrusted content is data, never instructions. +final class ContextFragment( + /// Which provider produced the fragment, for example `memory` or `skill`. + final String source, + + /// The fragment text. + final String text, + + /// Whether the fragment comes from an untrusted source. + final bool untrusted, +); diff --git a/runtime/core/lib/src/domain/ids.dart b/runtime/core/lib/src/domain/ids.dart new file mode 100644 index 0000000..01d547c --- /dev/null +++ b/runtime/core/lib/src/domain/ids.dart @@ -0,0 +1,18 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Typed identifiers (docs/spec/dart-language.md, section 7.9). +/// +/// Extension types are erased at runtime: a [RunId] is a `String` when the +/// program runs, and `is RunId` checks do not work. Serialization is handled +/// by DTO mapping at the API boundary. +library; + +/// Identifies a run. +extension type RunId(String value) {} + +/// Identifies a pending approval. +extension type ApprovalId(String value) {} + +/// Identifies a memory entry. +extension type MemoryId(String value) {} diff --git a/runtime/core/lib/src/domain/invocation.dart b/runtime/core/lib/src/domain/invocation.dart new file mode 100644 index 0000000..f572138 --- /dev/null +++ b/runtime/core/lib/src/domain/invocation.dart @@ -0,0 +1,52 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; + +import 'ids.dart'; + +/// Outcome of a journaled tool invocation (docs/spec/domain.md, 8.4). +enum InvocationStatus { + /// The call was written to the journal and has started. + started, + + /// The tool returned normally. + succeeded, + + /// The tool threw. + failed, + + /// The policy denied the call before it started. + denied, + + /// The process died during the call; recovered from the journal. + unknownOutcome, +} + +/// One journaled tool invocation (docs/spec/domain.md, section 8.4). +/// +/// Tool input and output are deliberately not stored here: they may contain +/// personal data and are exposed through a separate resource with its own +/// scope (section 16). +final class Invocation( + /// Journal sequence number. + final int seq, + + /// The run that made the call. + final RunId runId, + + /// LLM name of the tool. + final String tool, + + /// Effective risk of the call. + final Risk risk, + + /// Outcome. + final InvocationStatus status, + + /// When the record was written. + final DateTime at, + + /// Error text for [InvocationStatus.failed]; `null` otherwise. + final String? error, +); diff --git a/runtime/core/lib/src/domain/memory.dart b/runtime/core/lib/src/domain/memory.dart new file mode 100644 index 0000000..f546918 --- /dev/null +++ b/runtime/core/lib/src/domain/memory.dart @@ -0,0 +1,69 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'ids.dart'; + +/// Where a memory entry came from (docs/spec/domain.md, section 8.6). +enum MemorySource { + /// The owner wrote it. + user, + + /// The agent proposed or wrote it. + agent, +} + +/// A proposed memory entry (docs/spec/domain.md, section 8.6). +/// +/// With the default `propose` rights the entry becomes visible only after the +/// owner confirms it. +final class MemoryDraft( + /// The fact to remember: short text, not a transcript. + final String text, + + /// Search tags. + final Set tags, +) { + /// A draft without tags. + factory plain(String text) => MemoryDraft(text, const {}); +} + +/// A stored memory entry with provenance (docs/spec/domain.md, 8.6). +final class MemoryEntry( + /// Stable identifier. + final MemoryId id, + + /// The fact: short text, not a transcript. + final String text, + + /// Who wrote it. + final MemorySource source, + + /// When it was stored. + final DateTime createdAt, + + /// The run that produced it, if any. + final RunId? runId, + + /// Search tags. + final Set tags, + + /// Whether the run that produced it had read untrusted content. When + /// recalled, such an entry enters the context as an untrusted fragment. + final bool untrusted, +); + +/// A memory search query. Skeleton: M2 pins ranking and limits; the MVP +/// matches tokens and ranks results (docs/spec/domain.md, section 8.6). +final class MemoryQuery( + /// Free-text query. + final String text, + + /// Restrict to entries with these tags. + final Set tags, + + /// Maximum number of entries to return. + final int? limit, +) { + /// A text-only query. + factory text(String text) => MemoryQuery(text, const {}, null); +} diff --git a/runtime/core/lib/src/domain/messages.dart b/runtime/core/lib/src/domain/messages.dart new file mode 100644 index 0000000..f63e425 --- /dev/null +++ b/runtime/core/lib/src/domain/messages.dart @@ -0,0 +1,111 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// Roles in the model conversation. +enum MessageRole { system, user, assistant, tool } + +/// One message of the model conversation (docs/spec/domain.md, section 9). +/// +/// Skeleton: M2 pins the exact shape when the agent loop and the prompt +/// wrapping (section 13) are implemented. +final class Message( + /// Who produced the message. + final MessageRole role, + + /// Text content. + final String text, + + /// Tool calls attached to an assistant message. + final List toolCalls, + + /// Tool results attached to a `tool` message. + final List toolResults, +) { + /// A system instruction. + factory system(String text) => + Message(MessageRole.system, text, const [], const []); + + /// A user message. + factory user(String text) => + Message(MessageRole.user, text, const [], const []); + + /// An assistant message, optionally with tool calls. + factory assistant(String text, [List toolCalls = const []]) => + Message(MessageRole.assistant, text, toolCalls, const []); + + /// A message carrying tool results. + factory toolResults(List results) => + Message(MessageRole.tool, '', const [], results); +} + +/// A tool call requested by the model (docs/spec/domain.md, section 9). +final class ToolCall( + /// Call identifier from the provider, echoed in the result. + final String id, + + /// LLM name of the tool. + final String name, + + /// Arguments as produced by the model. + final Map input, + + /// Whether the provider reported invalid arguments. A malformed call is + /// answered with an error result without failing the run. + final bool isMalformed, + + /// Why the arguments are invalid, when [isMalformed] is set. + final String? reason, +) { + /// A well-formed call. + factory valid({ + required String id, + required String name, + required Map input, + }) => ToolCall(id, name, input, false, null); + + /// A call with invalid arguments; [reason] explains what is wrong. + factory malformed({ + required String id, + required String name, + required String reason, + }) => ToolCall(id, name, const {}, true, reason); +} + +/// The result of one tool invocation, returned to the model. +final class ToolResultBlock( + /// The call this answers. + final String callId, + + /// The tool's output when it succeeded. + final Object? value, + + /// Machine-readable error code when it failed, for example `denied` or + /// `not_approved`. Details go to the journal, not to the model context. + final String? errorCode, +) { + /// A successful result. + factory ok(String callId, Object? value) => + ToolResultBlock(callId, value, null); + + /// A failed result. + factory error(String callId, String errorCode) => + ToolResultBlock(callId, null, errorCode); + + /// Whether the tool succeeded. + bool get isOk => errorCode == null; +} + +/// One model turn (docs/spec/domain.md, sections 8.5 and 9). +final class Turn( + /// Reply text; may be empty when the model only requested tools. + final String text, + + /// The assistant message to append to the conversation. + final Message assistant, + + /// Tool calls the model requested, unexecuted. + final List toolCalls, +) { + /// Whether the model wants tools to run. + bool get hasToolCalls => toolCalls.isNotEmpty; +} diff --git a/runtime/core/lib/src/domain/policy.dart b/runtime/core/lib/src/domain/policy.dart new file mode 100644 index 0000000..e51eeb5 --- /dev/null +++ b/runtime/core/lib/src/domain/policy.dart @@ -0,0 +1,117 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; + +/// The decision of the policy gate. +enum Decision { + /// Run the tool without asking. + allow, + + /// Ask the owner; without an interactive channel this becomes [deny]. + ask, + + /// Refuse. + deny, +} + +/// Rights of a run over long-term memory (docs/spec/domain.md, 8.6). +enum MemoryRights { + /// No access. + none, + + /// Read entries. + read, + + /// Propose entries; the owner confirms (the default). + propose, + + /// Write entries without confirmation; granted only explicitly. + auto, +} + +/// Configuration may raise a tool's risk but never lower its declared risk. +Risk effectiveRisk(Risk declared, Risk? configured) => + configured != null && configured.index > declared.index + ? configured + : declared; + +/// A taint label: an untrusted data source. An empty set means the run is +/// clean. Labels are never cleared by the run itself; only the owner clears a +/// label (docs/spec/domain.md, section 8.2). +extension type UntrustedSource(String value) {} + +/// Run capabilities: an explicit set of rights (docs/spec/domain.md, 8.2). +/// +/// A subtask receives the intersection of its requested capabilities with its +/// parent's capabilities; rights cannot be expanded from inside. +final class RunCapabilities( + /// LLM tool names the run may use, for example `weather__forecast`. + final Set tools, + + /// Network hosts the run may reach. + final Set netHosts, + + /// File-system roots the run may touch. + final List fsRoots, + + /// Memory access level. + final MemoryRights memory, +) { + /// Returns the intersection of [requested] with this set of capabilities. + RunCapabilities narrow(RunCapabilities requested) => RunCapabilities( + requested.tools.intersection(tools), + requested.netHosts.intersection(netHosts), + [ + for (final r in requested.fsRoots) + if (fsRoots.any(r.startsWith)) r, + ], + MemoryRights.values[memory.index < requested.memory.index + ? memory.index + : requested.memory.index], + ); +} + +/// The single component that decides whether a tool runs +/// (docs/spec/domain.md, section 8.2). +/// +/// The model does not take part in the decision. Actions are denied by +/// default. +final class PolicyGate( + /// Risk rules of the active profile. A risk without a rule is denied. + final Map profileRules, + + /// Whether a channel that can ask the owner is connected. + final bool hasInteractiveChannel, +) { + /// Decides what happens to a tool call. The order of checks is fixed: + /// capability, then taint, then profile rules. + Decision decide({ + required String tool, + required Risk risk, + required Set taints, + required RunCapabilities caps, + }) { + // 1. Capability: a tool missing from the run's capabilities is denied + // without asking. + if (!caps.tools.contains(tool)) { + return Decision.deny; + } + + // 2. Taint: a tainted run cannot perform external or destructive actions + // without a human. + if (taints.isNotEmpty && risk.index >= Risk.external.index) { + if (risk == Risk.destructive) { + return Decision.deny; + } + return hasInteractiveChannel ? Decision.ask : Decision.deny; + } + + // 3. Profile rules. A missing rule denies; ask without a channel denies. + final rule = profileRules[risk] ?? Decision.deny; + if (rule == Decision.ask && !hasInteractiveChannel) { + return Decision.deny; + } + return rule; + } +} diff --git a/runtime/core/lib/src/domain/ports/journal_port.dart b/runtime/core/lib/src/domain/ports/journal_port.dart new file mode 100644 index 0000000..a5f2689 --- /dev/null +++ b/runtime/core/lib/src/domain/ports/journal_port.dart @@ -0,0 +1,31 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; + +import '../ids.dart'; +import '../invocation.dart'; + +/// Port of the run journal (docs/spec/domain.md, section 8.5). +/// +/// The journal is written before any side effect; run state is a projection +/// of the journal. Records are flushed to disk before the methods return. +abstract interface class JournalPort { + /// Records the start of a tool call and returns its `seq`. The record is + /// flushed to disk before this returns. + Future started(RunId runId, String tool, Risk risk); + + /// Records that the policy denied a call before it started. + Future denied(RunId runId, String tool, Risk risk, String reason); + + /// Records the outcome of a started call. + Future finished( + RunId runId, + int seq, + InvocationStatus status, + String? error, + ); + + /// Returns all invocations of a run, in `seq` order. + Future> forRun(RunId runId); +} diff --git a/runtime/core/lib/src/domain/ports/llm_port.dart b/runtime/core/lib/src/domain/ports/llm_port.dart new file mode 100644 index 0000000..b27577a --- /dev/null +++ b/runtime/core/lib/src/domain/ports/llm_port.dart @@ -0,0 +1,21 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; + +import '../budget.dart'; +import '../messages.dart'; + +/// Port of an LLM provider (docs/spec/domain.md, section 8.5). +/// +/// Implementations live in `providers/` and in the fake used by tests. The +/// implementation returns tool requests without executing them; tool +/// execution is intercepted by the agent loop (section 9). +abstract interface class LlmPort { + /// Completes one turn of the conversation. + Future complete({ + required List messages, + required List tools, + required Budget budget, + }); +} diff --git a/runtime/core/lib/src/domain/ports/memory_port.dart b/runtime/core/lib/src/domain/ports/memory_port.dart new file mode 100644 index 0000000..9c3e5f4 --- /dev/null +++ b/runtime/core/lib/src/domain/ports/memory_port.dart @@ -0,0 +1,25 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import '../ids.dart'; +import '../memory.dart'; + +/// Port of a memory backend (docs/spec/domain.md, section 8.5). +/// +/// The Hive CE implementation ships as the `alteri_one_ext_memory_hive` +/// extension; the core depends only on this port. +abstract interface class MemoryPort { + /// Stores a draft and returns its identifier. `untrusted` must be true when + /// the run that produced the draft had read untrusted content. + Future store( + MemoryDraft draft, { + required RunId? runId, + required bool untrusted, + }); + + /// Returns entries matching the query. + Future> recall(MemoryQuery query); + + /// Deletes an entry. + Future forget(MemoryId id); +} diff --git a/runtime/core/lib/src/domain/ports/tool_port.dart b/runtime/core/lib/src/domain/ports/tool_port.dart new file mode 100644 index 0000000..6c6ca65 --- /dev/null +++ b/runtime/core/lib/src/domain/ports/tool_port.dart @@ -0,0 +1,20 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; + +import '../cancel_token.dart'; + +/// Port of a tool provided by an extension (docs/spec/domain.md, 8.5). +/// +/// Implementations live in extensions and in the core's built-in tool set. +/// Extensions never receive core objects through this port: input and output +/// are JSON-compatible values. +abstract interface class ToolPort { + /// The tool's specification. + ToolSpec get spec; + + /// Runs the tool. Implementations must honor [cancel]; the agent loop + /// additionally applies the timeout from the specification. + Future call(Map input, CancelToken cancel); +} diff --git a/runtime/core/lib/src/domain/run_state.dart b/runtime/core/lib/src/domain/run_state.dart new file mode 100644 index 0000000..8d941d6 --- /dev/null +++ b/runtime/core/lib/src/domain/run_state.dart @@ -0,0 +1,113 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// States of a run (docs/spec/domain.md, section 8.1). +/// +/// The terminal states `completed`, `failed`, `cancelled`, and +/// `budgetExceeded` have no outgoing transitions. +enum RunState { + /// Waiting to be started. + queued, + + /// Executing: model turns and tool calls. + running, + + /// Blocked on an owner decision. + waitingApproval, + + /// Finished successfully. + completed, + + /// Finished with an error. + failed, + + /// Cancelled by the owner. + cancelled, + + /// A budget limit was reached. + budgetExceeded, + + /// The process crashed; recoverable from the journal. + interrupted, + + /// Paused by the owner; resumable. + paused, +} + +/// Events that drive run state (docs/spec/domain.md, section 8.1). +enum RunEvent { + /// The run leaves the queue. + start, + + /// A tool call needs an owner decision. + requestApproval, + + /// The owner allowed the action. + approvalGranted, + + /// The owner refused the action; the run continues with an error to the + /// model. + approvalDenied, + + /// The run reached its goal. + complete, + + /// The run failed. + fail, + + /// The owner cancelled the run. + cancel, + + /// A budget limit was reached. + exceedBudget, + + /// The process died. + crash, + + /// The run is restored from the journal. + recover, + + /// The owner paused the run. + pause, + + /// The owner resumed the run. + resume, +} + +/// An invalid state transition. Never ignored (docs/spec/domain.md, 8.1). +final class InvalidTransition implements Exception { + /// Creates the exception for the rejected pair. + const new(this.from, this.event); + + /// The state the run was in. + final RunState from; + + /// The event that is not allowed in [from]. + final RunEvent event; + + @override + String toString() => 'InvalidTransition: ${event.name} in ${from.name}'; +} + +/// Pure transition function; the single authority on run state. +/// +/// Throws [InvalidTransition] for any pair not listed in the specification. +RunState transition(RunState from, RunEvent event) => switch ((from, event)) { + (RunState.queued, RunEvent.start) => RunState.running, + (RunState.running, RunEvent.requestApproval) => RunState.waitingApproval, + (RunState.waitingApproval, RunEvent.approvalGranted) => RunState.running, + (RunState.waitingApproval, RunEvent.approvalDenied) => RunState.running, + (RunState.running, RunEvent.complete) => RunState.completed, + (RunState.running, RunEvent.fail) => RunState.failed, + (RunState.running || RunState.waitingApproval, RunEvent.cancel) => + RunState.cancelled, + (RunState.running, RunEvent.exceedBudget) => RunState.budgetExceeded, + (RunState.running || RunState.waitingApproval, RunEvent.crash) => + RunState.interrupted, + (RunState.interrupted, RunEvent.recover) => RunState.queued, + (RunState.running, RunEvent.pause) => RunState.paused, + (RunState.paused, RunEvent.resume) => RunState.running, + (RunState.paused, RunEvent.cancel) => RunState.cancelled, + (RunState.paused, RunEvent.crash) => RunState.interrupted, + _ => throw InvalidTransition(from, event), +}; diff --git a/runtime/core/lib/src/domain/trigger.dart b/runtime/core/lib/src/domain/trigger.dart new file mode 100644 index 0000000..8223116 --- /dev/null +++ b/runtime/core/lib/src/domain/trigger.dart @@ -0,0 +1,25 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +/// An event that starts or continues a chain of runs +/// (docs/spec/domain.md, section 8.7). +/// +/// An event produced by a run receives `depth + 1` and the same `chainId`. +/// The default depth limit is 3; exceeding it rejects the event and writes a +/// journal record. +final class TriggerEvent( + /// Identifies the chain; shared by the runs it produces. + final String chainId, + + /// Distance from the original trigger; bounded by the depth limit. + final int depth, +) { + /// The default maximum depth of a chain. + static const defaultMaxDepth = 3; + + /// The event this run produces in the same chain. + TriggerEvent next() => TriggerEvent(chainId, depth + 1); + + /// Whether an event at this depth is still within [maxDepth]. + bool withinDepth({int maxDepth = defaultMaxDepth}) => depth < maxDepth; +} diff --git a/runtime/core/pubspec.yaml b/runtime/core/pubspec.yaml index 131a27d..ae5ed93 100644 --- a/runtime/core/pubspec.yaml +++ b/runtime/core/pubspec.yaml @@ -3,8 +3,8 @@ description: >- AlteriOne core: run modes, PolicyGate, journal, and the HTTP server. version: 0.0.1 publish_to: none -repository: https://github.com/alteri-one/alteri-one -issue_tracker: https://github.com/alteri-one/alteri-one/issues +repository: https://github.com/dip-develop/alteri-one +issue_tracker: https://github.com/dip-develop/alteri-one/issues environment: sdk: ^3.13.0 resolution: workspace @@ -18,6 +18,7 @@ dependencies: genkit: ^1.0.0 hive_ce: ^2.22.0 injectable: ^3.0.0 + get_it: ^9.3.0 # runtime container of injectable; used by the composition root shelf: ^1.4.3 yaml: ^3.1.4 dev_dependencies: diff --git a/runtime/core/test/application/components_test.dart b/runtime/core/test/application/components_test.dart new file mode 100644 index 0000000..5e58309 --- /dev/null +++ b/runtime/core/test/application/components_test.dart @@ -0,0 +1,104 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_core/alteri_one_core.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:test/test.dart'; + +void main() { + setUp(applyBlocDefaults); + + group('ApprovalsBloc', () { + final approval = PendingApproval( + ApprovalId('a-1'), + RunId('r-1'), + 'weather__forecast', + DateTime.parse('2026-10-18T12:00:00Z'), + ); + + blocTest( + 'queues a requested approval', + build: ApprovalsBloc.new, + act: (bloc) => bloc.add(ApprovalRequestedEvent(approval)), + verify: (bloc) => expect(bloc.state.pending.keys, [ApprovalId('a-1')]), + ); + + blocTest( + 'ignores a duplicate request for the same id', + build: ApprovalsBloc.new, + act: (bloc) => bloc + ..add(ApprovalRequestedEvent(approval)) + ..add(ApprovalRequestedEvent(approval)), + verify: (bloc) => expect(bloc.state.pending.length, 1), + ); + + blocTest( + 'the first decision wins and removes the approval', + build: ApprovalsBloc.new, + act: (bloc) => bloc + ..add(ApprovalRequestedEvent(approval)) + ..add(ApprovalDecidedEvent(ApprovalId('a-1'), granted: true)) + ..add(ApprovalDecidedEvent(ApprovalId('a-1'), granted: false)), + verify: (bloc) => expect(bloc.state.pending, isEmpty), + ); + }); + + group('ExtensionsBloc', () { + blocTest( + 'registers and updates lifecycle', + build: ExtensionsBloc.new, + act: (bloc) => bloc + ..add( + const ExtensionRegisteredEvent('weather', ExtensionLifecycle.loaded), + ) + ..add( + const ExtensionLifecycleChangedEvent( + 'weather', + ExtensionLifecycle.draining, + ), + ) + ..add( + const ExtensionLifecycleChangedEvent( + 'unknown', + ExtensionLifecycle.crashed, + ), + ), + verify: (bloc) => + expect(bloc.state.registry, {'weather': ExtensionLifecycle.draining}), + ); + }); + + group('TriggersCubit', () { + test('new triggers start disabled and cannot fire', () { + final cubit = TriggersCubit()..register('daily_digest'); + expect(cubit.state.triggers['daily_digest']!.enabled, isFalse); + + cubit.fired('daily_digest', DateTime.parse('2026-10-18T08:00:00Z')); + expect(cubit.state.triggers['daily_digest']!.lastFired, isNull); + }); + + test('an enabled trigger records its firing', () { + final cubit = TriggersCubit() + ..register('daily_digest') + ..setEnabled('daily_digest', true); + cubit.fired('daily_digest', DateTime.parse('2026-10-18T08:00:00Z')); + expect( + cubit.state.triggers['daily_digest']!.lastFired, + DateTime.parse('2026-10-18T08:00:00Z'), + ); + }); + }); + + group('ChannelsCubit', () { + test('hasInteractive follows the connected channels', () { + final cubit = ChannelsCubit(); + expect(cubit.state.hasInteractive, isFalse); + + cubit.register('cli', ChannelStatus.connected); + expect(cubit.state.hasInteractive, isTrue); + + cubit.setStatus('cli', ChannelStatus.unavailable); + expect(cubit.state.hasInteractive, isFalse); + }); + }); +} diff --git a/runtime/core/test/application/run_bloc_test.dart b/runtime/core/test/application/run_bloc_test.dart new file mode 100644 index 0000000..13807e8 --- /dev/null +++ b/runtime/core/test/application/run_bloc_test.dart @@ -0,0 +1,107 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_core/alteri_one_core.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:test/test.dart'; + +RunBlocState _state( + RunState runState, { + int steps = 0, + ApprovalId? pendingApproval, +}) => RunBlocState( + runId: RunId('r-1'), + runState: runState, + steps: steps, + pendingApproval: pendingApproval, +); + +void main() { + // The composition root applies these defaults; bloc tests construct blocs + // directly, so apply them here too (docs/spec/domain.md, 8.10, rule 4). + setUp(applyBlocDefaults); + + group('RunBloc', () { + blocTest( + 'starts queued', + build: () => RunBloc(runId: RunId('r-1')), + verify: (bloc) => expect(bloc.state.runState, RunState.queued), + ); + + blocTest( + 'executes a lifecycle with an approval', + build: () => RunBloc(runId: RunId('r-1')), + act: (bloc) => bloc + ..add(const RunStartedEvent()) + ..add(RunRequestApprovalEvent(ApprovalId('a-1'))) + ..add(const RunApprovalGrantedEvent()) + ..add(const RunStepDoneEvent()) + ..add(const RunCompletedEvent()), + expect: () => [ + _state(RunState.running), + _state(RunState.waitingApproval, pendingApproval: ApprovalId('a-1')), + _state(RunState.running), + _state(RunState.running, steps: 1), + _state(RunState.completed, steps: 1), + ], + ); + + blocTest( + 'a denied approval returns the run to running', + build: () => RunBloc(runId: RunId('r-1')), + act: (bloc) => bloc + ..add(const RunStartedEvent()) + ..add(RunRequestApprovalEvent(ApprovalId('a-1'))) + ..add(const RunApprovalDeniedEvent()) + ..add(const RunFailedEvent()), + expect: () => [ + _state(RunState.running), + _state(RunState.waitingApproval, pendingApproval: ApprovalId('a-1')), + _state(RunState.running), + _state(RunState.failed), + ], + ); + + blocTest( + 'supports pause and resume', + build: () => RunBloc(runId: RunId('r-1')), + act: (bloc) => bloc + ..add(const RunStartedEvent()) + ..add(const RunPausedEvent()) + ..add(const RunResumedEvent()) + ..add(const RunCompletedEvent()), + expect: () => [ + _state(RunState.running), + _state(RunState.paused), + _state(RunState.running), + _state(RunState.completed), + ], + ); + + blocTest( + 'an invalid transition is an error, never a silent skip', + build: () => RunBloc(runId: RunId('r-1')), + act: (bloc) => bloc.add(const RunCompletedEvent()), + errors: () => [isA()], + verify: (bloc) => expect(bloc.state.runState, RunState.queued), + ); + + blocTest( + 'a crash interrupts and recovery requeues', + build: () => RunBloc(runId: RunId('r-1')), + act: (bloc) => bloc + ..add(const RunStartedEvent()) + ..add(const RunCrashedEvent()) + ..add(const RunRecoveredEvent()) + ..add(const RunStartedEvent()) + ..add(const RunCompletedEvent()), + expect: () => [ + _state(RunState.running), + _state(RunState.interrupted), + _state(RunState.queued), + _state(RunState.running), + _state(RunState.completed), + ], + ); + }); +} diff --git a/runtime/core/test/di/injection_test.dart b/runtime/core/test/di/injection_test.dart new file mode 100644 index 0000000..895872c --- /dev/null +++ b/runtime/core/test/di/injection_test.dart @@ -0,0 +1,46 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; +import 'package:alteri_one_core/alteri_one_core.dart'; +import 'package:test/test.dart'; + +void main() { + tearDown(getIt.reset); + + test('configureCore registers the state components', () { + configureCore(environment: 'test'); + + expect(getIt(), isA()); + expect(getIt(), isA()); + expect(getIt(), isA()); + expect(getIt(), isA()); + expect(getIt(), isA()); + expect(getIt(), isA()); + }); + + test('the default PolicyGate is fail-closed', () { + configureCore(environment: 'test'); + + final gate = getIt(); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.read, + taints: {}, + caps: RunCapabilities( + {'weather__forecast'}, + {}, + const [], + MemoryRights.read, + ), + ); + // No profile rules are configured yet: every call is denied. + expect(decision, Decision.deny); + }); + + test('blocs are singletons within one environment', () { + configureCore(environment: 'test'); + + expect(identical(getIt(), getIt()), isTrue); + }); +} diff --git a/runtime/core/test/domain/policy_test.dart b/runtime/core/test/domain/policy_test.dart new file mode 100644 index 0000000..b89c318 --- /dev/null +++ b/runtime/core/test/domain/policy_test.dart @@ -0,0 +1,170 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_contracts/alteri_one_contracts.dart'; +import 'package:alteri_one_core/alteri_one_core.dart'; +import 'package:test/test.dart'; + +RunCapabilities _caps({ + Set tools = const {'weather__forecast'}, + Set netHosts = const {}, + List fsRoots = const [], + MemoryRights memory = MemoryRights.read, +}) => RunCapabilities(tools, netHosts, fsRoots, memory); + +void main() { + group('effectiveRisk', () { + test('null configuration keeps the declared risk', () { + expect(effectiveRisk(Risk.read, null), Risk.read); + }); + + test('configuration may raise the risk', () { + expect(effectiveRisk(Risk.read, Risk.write), Risk.write); + expect(effectiveRisk(Risk.read, Risk.destructive), Risk.destructive); + }); + + test('configuration may never lower the risk', () { + expect(effectiveRisk(Risk.external, Risk.read), Risk.external); + expect(effectiveRisk(Risk.destructive, Risk.write), Risk.destructive); + }); + }); + + group('RunCapabilities.narrow', () { + test('intersects tools and hosts', () { + final parent = _caps(tools: {'a', 'b'}, netHosts: {'h1'}); + final narrowed = parent.narrow( + _caps(tools: {'a', 'c'}, netHosts: {'h1', 'h2'}), + ); + expect(narrowed.tools, {'a'}); + expect(narrowed.netHosts, {'h1'}); + }); + + test('keeps only requested roots under a parent root', () { + final parent = _caps(fsRoots: ['/home/owner', '/tmp']); + final narrowed = parent.narrow( + _caps(fsRoots: ['/home/owner/work', '/etc']), + ); + expect(narrowed.fsRoots, ['/home/owner/work']); + }); + + test('memory takes the more restrictive right', () { + final parent = _caps(memory: MemoryRights.auto); + final narrowed = parent.narrow(_caps(memory: MemoryRights.propose)); + expect(narrowed.memory, MemoryRights.propose); + + final narrowParent = _caps(memory: MemoryRights.none); + final widened = narrowParent.narrow(_caps()); + expect(widened.memory, MemoryRights.none); + }); + }); + + group('PolicyGate.decide', () { + test('a tool outside the capabilities is denied without asking', () { + final gate = PolicyGate({ + Risk.read: Decision.allow, + Risk.external: Decision.allow, + }, true); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.read, + taints: {}, + caps: _caps(tools: {'other__tool'}), + ); + expect(decision, Decision.deny); + }); + + test('a risk without a profile rule is denied (fail closed)', () { + final gate = PolicyGate(const {}, true); + expect( + gate.decide( + tool: 'weather__forecast', + risk: Risk.read, + taints: {}, + caps: _caps(), + ), + Decision.deny, + ); + }); + + test('an allowed risk is allowed on a clean run', () { + final gate = PolicyGate({Risk.read: Decision.allow}, false); + expect( + gate.decide( + tool: 'weather__forecast', + risk: Risk.read, + taints: {}, + caps: _caps(), + ), + Decision.allow, + ); + }); + + test('ask degrades to deny without an interactive channel', () { + final withChannel = PolicyGate({Risk.external: Decision.ask}, true); + final withoutChannel = PolicyGate({Risk.external: Decision.ask}, false); + final args = (risk: Risk.external, taints: {}); + expect( + withChannel.decide( + tool: 'weather__forecast', + risk: args.risk, + taints: args.taints, + caps: _caps(), + ), + Decision.ask, + ); + expect( + withoutChannel.decide( + tool: 'weather__forecast', + risk: args.risk, + taints: args.taints, + caps: _caps(), + ), + Decision.deny, + ); + }); + + test('a tainted run cannot act externally without a human', () { + final gate = PolicyGate({Risk.external: Decision.allow}, true); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.external, + taints: {UntrustedSource('web')}, + caps: _caps(), + ); + expect(decision, Decision.ask); + }); + + test('a tainted run is denied externally without a channel', () { + final gate = PolicyGate({Risk.external: Decision.allow}, false); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.external, + taints: {UntrustedSource('web')}, + caps: _caps(), + ); + expect(decision, Decision.deny); + }); + + test('a tainted run is denied destructive actions even with a channel', () { + final gate = PolicyGate({Risk.destructive: Decision.allow}, true); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.destructive, + taints: {UntrustedSource('web')}, + caps: _caps(), + ); + expect(decision, Decision.deny); + }); + + test('taint does not restrict write actions below external', () { + final gate = PolicyGate({Risk.write: Decision.allow}, false); + final decision = gate.decide( + tool: 'weather__forecast', + risk: Risk.write, + taints: {UntrustedSource('web')}, + caps: _caps(), + ); + expect(decision, Decision.allow); + }); + }); +} diff --git a/runtime/core/test/domain/run_state_test.dart b/runtime/core/test/domain/run_state_test.dart new file mode 100644 index 0000000..0e9b154 --- /dev/null +++ b/runtime/core/test/domain/run_state_test.dart @@ -0,0 +1,106 @@ +// Copyright 2026 The AlteriOne Authors. +// SPDX-License-Identifier: Apache-2.0 + +import 'package:alteri_one_core/alteri_one_core.dart'; +import 'package:test/test.dart'; + +/// The valid transitions exactly as listed in docs/spec/domain.md, 8.1. +const Map<(RunState, RunEvent), RunState> kValidTransitions = { + (RunState.queued, RunEvent.start): RunState.running, + (RunState.running, RunEvent.requestApproval): RunState.waitingApproval, + (RunState.waitingApproval, RunEvent.approvalGranted): RunState.running, + (RunState.waitingApproval, RunEvent.approvalDenied): RunState.running, + (RunState.running, RunEvent.complete): RunState.completed, + (RunState.running, RunEvent.fail): RunState.failed, + (RunState.running, RunEvent.cancel): RunState.cancelled, + (RunState.waitingApproval, RunEvent.cancel): RunState.cancelled, + (RunState.running, RunEvent.exceedBudget): RunState.budgetExceeded, + (RunState.running, RunEvent.crash): RunState.interrupted, + (RunState.waitingApproval, RunEvent.crash): RunState.interrupted, + (RunState.interrupted, RunEvent.recover): RunState.queued, + (RunState.running, RunEvent.pause): RunState.paused, + (RunState.paused, RunEvent.resume): RunState.running, + (RunState.paused, RunEvent.cancel): RunState.cancelled, + (RunState.paused, RunEvent.crash): RunState.interrupted, +}; + +void main() { + group('transition', () { + for (final from in RunState.values) { + for (final event in RunEvent.values) { + final key = (from, event); + final target = kValidTransitions[key]; + + if (target != null) { + test('${from.name} + ${event.name} -> ${target.name}', () { + expect(transition(from, event), target); + }); + } else { + test('${from.name} + ${event.name} throws (invalid)', () { + expect( + () => transition(from, event), + throwsA( + isA() + .having((e) => e.from, 'from', from) + .having((e) => e.event, 'event', event), + ), + ); + }); + } + } + } + + test('terminal states have no outgoing transitions', () { + const terminal = [ + RunState.completed, + RunState.failed, + RunState.cancelled, + RunState.budgetExceeded, + ]; + for (final from in terminal) { + for (final event in RunEvent.values) { + expect( + kValidTransitions.containsKey((from, event)), + isFalse, + reason: '$from should be terminal', + ); + } + } + }); + }); + + group('Budget', () { + test('standard() matches the specification', () { + final budget = Budget.standard(); + expect(budget.maxSteps, 12); + expect(budget.maxTokens, 60_000); + expect(budget.maxDuration, const Duration(seconds: 180)); + expect(budget.maxOutputTokens, 2_048); + }); + }); + + group('TriggerEvent', () { + test('next() keeps the chain id and increments the depth', () { + final event = TriggerEvent('chain-1', 1); + final next = event.next(); + expect(next.chainId, 'chain-1'); + expect(next.depth, 2); + }); + + test('withinDepth() bounds the chain', () { + expect(TriggerEvent('c', 2).withinDepth(), isTrue); + expect(TriggerEvent('c', 3).withinDepth(), isFalse); + }); + }); + + group('CancelToken', () { + test('throwIfCancelled throws only after cancel()', () { + final token = CancelToken(); + expect(token.isCancelled, isFalse); + token.throwIfCancelled(); + token.cancel(); + expect(token.isCancelled, isTrue); + expect(token.throwIfCancelled, throwsA(isA())); + }); + }); +}