Skip to content
Merged
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ v8-runner launch mcp va --mcp-port 1550 --wait-ready
`test va`, MCP `run_all_tests` с `runner=vanessa` или `launch mcp va --wait-ready`; голый
`launch mcp` предназначен только для client MCP без загрузки Vanessa.

Для автоматизации `v8-runner --json-message launch ...` сохраняет поле `binary` и добавляет
canonical `platform_resolution` (path, version, source и installation root). Эта metadata
публикуется только для результата `launch`, а не для всех команд.

### Поднимите MCP transport (MCP-транспорт) для AI-агентов:

```bash
Expand Down
5 changes: 4 additions & 1 deletion SKILL/references/config-and-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ settings before CLI overrides.
- `builder`: `DESIGNER` or `IBCMD`.
- `infobase.connection`: often `File=build/ib` for local automation.
- `source-set`: ordered configuration and extension sources.
- `tools.platform.path` or `tools.platform.version`: 1C platform discovery hints.
- `tools.platform.path`, `version`, and `strict`: platform discovery hints. `path` is always an
explicit-only boundary with no default-root or `PATH` fallback. Without `path`, `version` filters
normal discovery. With `path`, `version` is ignored unless `strict: true`; strict path+version
resolution rejects unknown or mismatched versions and pins sibling utilities to one canonical root.
- `tools.edt_cli.path`, `version`, and `interactive-mode`: EDT CLI discovery and execution mode.
- `tests.yaxunit` and `tests.va`: test runner configuration.
- `tools.client_mcp`, `tools.va`, and `tools.enterprise`: launch and client-side MCP integration hints.
Expand Down
7 changes: 7 additions & 0 deletions docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,13 @@ v8-runner launch mcp [va] [--mode <thin|thick|ordinary>] [--wait-ready] [FLAGS]
`--raw-key` не может задавать `/C`, `/Execute` или `/Out`.
- Для `designer`/`thin`/`thick`/`ordinary` дополнительные typed flags: `--c`, `--execute`, `--use-privileged-mode`, `--output`,
повторяемый `--raw-key`.
- Platform discovery использует `tools.platform.path` как explicit-only границу: если path задан,
default roots и `PATH` не используются. `tools.platform.version` без path фильтрует обычный
поиск; вместе с path проверяется только при `tools.platform.strict: true`, а при
`strict: false` игнорируется.
- JSON-результат именно `launch` содержит legacy `binary` и `platform_resolution` с canonical
`path`, `version` (или `null`), `source` (`explicit`, `default-root` или `path`) и
`installation_root`. Это не общий metadata contract для остальных команд.

### `mcp serve`

Expand Down
32 changes: 32 additions & 0 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ tools:
epf_path: /path/to/vanessa.epf
platform:
path: /opt/1cv8/x86_64
strict: true
version: 8.3.27.1859
enterprise:
additional-launch-keys:
Expand Down Expand Up @@ -508,6 +509,26 @@ source-set build, а `launch mcp` и `launch mcp va` расширение не
- на каталог `bin`;
- на корень установки с версиями.

Относительный путь нормализуется относительно каталога primary `v8project.yaml`.
Если `path` задан, поиск platform utilities ограничивается этим путём и не переходит к default
roots или `PATH`, независимо от `strict`.

### `tools.platform.strict`

- Тип: boolean
- Обязателен: нет
- По умолчанию: `false`

`strict` управляет проверкой `tools.platform.version` внутри configured `path`. Сам `path` всегда
является explicit-only границей. При `strict: false` значение `version` для configured `path`
игнорируется. При `strict: true` найденная внутри `path` utility обязана соответствовать
`version`; неизвестная версия или несовпадение версии завершают команду ошибкой.

Если `path` указывает на конкретный executable, поиск sibling utilities (`1cv8`, `1cv8c`,
`ibcmd`) в `strict: false` идёт рядом с указанным файлом, а в `strict: true` — рядом с его
canonical installation. При `strict: true` первая найденная platform utility фиксирует один
canonical installation root; последующие `1cv8`, `1cv8c` и `ibcmd` выбираются только из этого root.

### `tools.platform.version`

- Тип: строка
Expand All @@ -520,6 +541,17 @@ source-set build, а `launch mcp` и `launch mcp va` расширение не
- `8.3.20`: выбирается максимальная найденная сборка `8.3.20.*`;
- `8.3`: выбирается максимальная найденная версия `8.3.*.*`.

Матрица поведения:

| Конфигурация | Поведение |
| --- | --- |
| `version`, без `path` | Поиск по default roots и `PATH` с проверкой версии. |
| `path + version`, `strict: false` | Поиск только по `path`; `version` игнорируется. |
| `path + version`, `strict: true` | Поиск только по `path`; версия обязана совпасть. |
| `path`, без `version` | Поиск только по `path`; проверки версии нет. |
| Без `path` и без `version` | Обычный поиск по default roots и `PATH`. |
| `strict: true`, без `path` | Не создаёт boundary; с `version` работает как version-only поиск, без `version` не меняет обычный поиск. |

## `tools.enterprise`

### `tools.enterprise.additional-launch-keys`
Expand Down
9 changes: 7 additions & 2 deletions docs/schemas/v8project.local.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,19 @@
"additionalProperties": false,
"properties": {
"path": {
"description": "Platform binary, installation `bin` directory, or platform root discovery hint.",
"description": "Platform binary, installation `bin` directory, or platform root discovery hint. When set, platform discovery uses only this path and never falls back to default roots or PATH.",
"type": [
"string",
"null"
]
},
"strict": {
"default": false,
"description": "Enforce `version` for a configured `path`. Without `path`, `version` is applied to normal default-root and PATH discovery.",
"type": "boolean"
},
"version": {
"description": "Platform version requirement used for discovery.",
"description": "Platform version requirement. Without `path`, it filters normal discovery; with `path`, it is ignored unless `strict` is true.",
"type": [
"string",
"null"
Expand Down
9 changes: 7 additions & 2 deletions docs/schemas/v8project.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,14 +287,19 @@
"additionalProperties": false,
"properties": {
"path": {
"description": "Platform binary, installation `bin` directory, or platform root discovery hint.",
"description": "Platform binary, installation `bin` directory, or platform root discovery hint. When set, platform discovery uses only this path and never falls back to default roots or PATH.",
"type": [
"string",
"null"
]
},
"strict": {
"default": false,
"description": "Enforce `version` for a configured `path`. Without `path`, `version` is applied to normal default-root and PATH discovery.",
"type": "boolean"
},
"version": {
"description": "Platform version requirement used for discovery.",
"description": "Platform version requirement. Without `path`, it filters normal discovery; with `path`, it is ignored unless `strict` is true.",
"type": [
"string",
"null"
Expand Down
31 changes: 31 additions & 0 deletions docs/superpowers/plans/2026-07-22-strict-platform-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Strict Platform Resolution Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add fail-closed platform pinning with coherent installation resolution and observable launch metadata.

**Architecture:** Add a typed resolution policy at the config-to-locator boundary. Candidates carry their source; any configured platform path searches only that explicit boundary, while strict path resolution also enforces version and pins one canonical installation root for all platform utilities.

**Tech Stack:** Rust, serde, schemars, clap integration tests, existing locator and launch contracts.

## Task 1: Configuration and path normalization

- [ ] Add failing model/schema/loader tests for strict, relative platform paths, and strict without path.
- [ ] Add `PlatformToolConfig.strict`, schema fields, strict-without-path acceptance, and path normalization.
- [ ] Regenerate both checked-in schemas and run focused config tests.

## Task 2: Typed strict locator

- [ ] Add failing locator tests for the path/version/strict contract matrix, exact/prefix mismatch, unknown version, and sibling consistency.
- [ ] Add typed policy/source/errors and source-aware candidates.
- [ ] Make configured path resolution explicit-only, bind strict path resolution to one canonical installation root, and capture PATH roots once.
- [ ] Run the complete locator and utilities suites.

## Task 3: JSON and documentation

- [ ] Add a failing launch JSON test for path/version/source/root metadata.
- [ ] Extend LaunchResult and mapping without removing the existing binary field.
- [ ] Update configuration, capabilities, and repo-local skill guidance.
- [ ] Run formatting, focused integration tests, all-target check, and clippy.
- [ ] Run independent tester, reviewer, and Rust expert passes; resolve or waive every finding.
- [ ] Commit, push, and create the upstream PR.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Strict Platform Resolution Design

## Goal

Make a configured 1C platform path an explicit search boundary and make version enforcement
predictable across path and non-path discovery.

## Configuration contract

Maintainer decision: `tools.platform.path` itself means "search only here". It does not fall back to
default roots or `PATH`, regardless of `strict`. `strict` is a boolean with default `false`; it does
not require `tools.platform.path`.

Contract matrix:

| Configuration | Behavior |
| --- | --- |
| `version`, no `path` | Search default roots and `PATH` with version filtering. |
| `path + version`, `strict: false` | Search only `path`; ignore `version`. |
| `path + version`, `strict: true` | Search only `path`; require a matching version. |
| `path`, no `version` | Search only `path`; do not check version. |
| No `path`, no `version` | Normal default-root and `PATH` discovery. |
| `strict: true`, no `path` | No boundary is created; with `version`, behaves like version-only discovery; without `version`, it is a no-op. |

Version requirements retain the existing semantics when they are applied: four components are exact;
two or three components are prefixes and select the highest matching installation.

## Installation consistency

When `strict: true` is combined with `path`, the first successfully resolved platform utility binds
the locator to its canonical installation root. Later resolution of `1cv8`, `1cv8c`, or `ibcmd`
uses only a direct or `bin` sibling below that root. A sibling elsewhere is rejected. For a file
path hint, `strict: false` resolves lexical siblings next to the configured file; `strict: true`
resolves siblings from the canonical installation.

Each location carries a typed resolution source (`explicit`, `default-root`, or `path`), an absolute canonical executable path, inferred version, and canonical installation root.

## JSON scope

`launch` already exposes its selected binary as a public result. It will additionally expose structured platform resolution metadata containing absolute path, version, source, and installation root. Extending every command result would require a separate shared-envelope contract migration and is outside this focused locator fix.

## Verification

TDD covers version-only discovery, path-only no-fallback behavior, lenient path+version ignoring,
strict path+version mismatch and unknown-version errors, versioned-root selection, sibling
consistency, relative path normalization, schema validation, and launch JSON metadata.
74 changes: 74 additions & 0 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ fn normalize_config_paths(config: &mut AppConfig, config_dir: &Path) {
if let Some(path) = config.tools.va.epf_path.as_mut() {
*path = normalize_optional_path(path, config_dir);
}
if let Some(path) = config.tools.platform.path.as_mut() {
*path = normalize_optional_path(path, config_dir);
}
if let Some(extension) = config.tools.client_mcp.extension.as_mut() {
if let Some(source) = extension.source_mut() {
source.path = normalize_optional_path(&source.path, config_dir);
Expand Down Expand Up @@ -1244,4 +1247,75 @@ mod tests {
Some("1c-edt-2025.2.3")
);
}

#[test]
fn load_config_defaults_platform_strict_to_false() {
let dir = tempdir().expect("tempdir");
let base = dir.path().join("base");
let work = dir.path().join("work");
let src = base.join("src");
std::fs::create_dir_all(&src).expect("src dir");
let config_path = dir.path().join("v8project.yaml");
std::fs::write(
&config_path,
format!(
"workPath: {}\nformat: DESIGNER\nbuilder: DESIGNER\ninfobase:\n connection: \"File=/tmp/ib\"\nsource-set:\n - name: main\n type: CONFIGURATION\n path: base/src\n",
work.display()
),
)
.expect("write config");

let config = load_config(config_path.to_str(), None).expect("load config");

assert!(!config.tools.platform.strict);
}

#[test]
fn load_config_accepts_strict_platform_without_path() {
let dir = tempdir().expect("tempdir");
let base = dir.path().join("base");
let work = dir.path().join("work");
let src = base.join("src");
std::fs::create_dir_all(&src).expect("src dir");
let config_path = dir.path().join("v8project.yaml");
std::fs::write(
&config_path,
format!(
"workPath: {}\nformat: DESIGNER\nbuilder: DESIGNER\ninfobase:\n connection: \"File=/tmp/ib\"\ntools:\n platform:\n strict: true\nsource-set:\n - name: main\n type: CONFIGURATION\n path: base/src\n",
work.display()
),
)
.expect("write config");

let config = load_config(config_path.to_str(), None).expect("strict without path");

assert!(config.tools.platform.strict);
assert!(config.tools.platform.path.is_none());
}

#[test]
fn load_config_normalizes_relative_platform_path_against_config_directory() {
let dir = tempdir().expect("tempdir");
let base = dir.path().join("base");
let work = dir.path().join("work");
let src = base.join("src");
std::fs::create_dir_all(&src).expect("src dir");
let config_path = dir.path().join("v8project.yaml");
std::fs::write(
&config_path,
format!(
"workPath: {}\nformat: DESIGNER\nbuilder: DESIGNER\ninfobase:\n connection: \"File=/tmp/ib\"\ntools:\n platform:\n path: platform/bin\n strict: false\nsource-set:\n - name: main\n type: CONFIGURATION\n path: base/src\n",
work.display()
),
)
.expect("write config");

let config = load_config(config_path.to_str(), None).expect("load config");
let config_dir = std::fs::canonicalize(dir.path()).expect("canonical config dir");

assert_eq!(
config.tools.platform.path.as_deref(),
Some(config_dir.join("platform/bin").as_path())
);
}
}
24 changes: 24 additions & 0 deletions src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,15 @@ pub struct PlatformToolConfig {
/// directory, or to a platform root that contains versioned subdirectories.
pub path: Option<PathBuf>,

/// Enforce `version` for a configured `path`.
///
/// `path` is always an explicit-only search boundary. When `strict` is `false`,
/// `version` is ignored for that path; when `strict` is `true`, the executable
/// found inside the path must match `version`. Without `path`, `version` is
/// applied to normal default-root and PATH discovery.
#[serde(default)]
pub strict: bool,

/// Platform version requirement in `major.minor`, `major.minor.patch`, or
/// `major.minor.patch.build` format.
///
Expand Down Expand Up @@ -651,3 +660,18 @@ const fn default_edt_cli_startup_timeout_ms() -> u64 {
const fn default_edt_cli_command_timeout_ms() -> u64 {
300_000
}

#[cfg(test)]
mod tests {
use super::PlatformToolConfig;

#[test]
fn platform_strict_defaults_to_false_and_deserializes_true() {
let default = PlatformToolConfig::default();
assert!(!default.strict);

let configured: PlatformToolConfig =
serde_yaml::from_str("strict: true\n").expect("deserialize strict platform config");
assert!(configured.strict);
}
}
Loading