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
4 changes: 3 additions & 1 deletion SKILL/references/config-and-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ 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. `strict` defaults to
`false`; when set to `true`, `path` is required and resolution fails closed inside one canonical
installation root (no default-root or `PATH` fallback, including for unknown pinned versions).
- `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
3 changes: 3 additions & 0 deletions docs/CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,9 @@ 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`.
- JSON-результат именно `launch` содержит legacy `binary` и `platform_resolution` с canonical
`path`, `version` (или `null`), `source` (`explicit`, `default-root` или `path`) и
`installation_root`. Это не общий metadata contract для остальных команд.

### `mcp serve`

Expand Down
19 changes: 19 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,21 @@ source-set build, а `launch mcp` и `launch mcp va` расширение не
- на каталог `bin`;
- на корень установки с версиями.

Относительный путь нормализуется относительно каталога primary `v8project.yaml`.

### `tools.platform.strict`

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

При `strict: true` поле `tools.platform.path` обязательно. Поиск ограничивается указанной
установкой: отсутствующая utility, неизвестная версия при заданном `tools.platform.version` или
несовпадение версии завершают команду ошибкой без fallback к default roots или `PATH`.
Первая найденная platform utility фиксирует один canonical installation root; последующие
`1cv8`, `1cv8c` и `ibcmd` выбираются только из этого root. При `strict: false` сохранён legacy
порядок: explicit path, default roots, затем `PATH`.

### `tools.platform.version`

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

В strict mode version requirement не допускает неизвестную версию: такая установка отклоняется
вместо fallback.

## `tools.enterprise`

### `tools.enterprise.additional-launch-keys`
Expand Down
5 changes: 5 additions & 0 deletions docs/schemas/v8project.local.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,11 @@
"null"
]
},
"strict": {
"default": false,
"description": "Require platform utility resolution to stay within the configured path.",
"type": "boolean"
},
"version": {
"description": "Platform version requirement used for discovery.",
"type": [
Expand Down
5 changes: 5 additions & 0 deletions docs/schemas/v8project.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@
"null"
]
},
"strict": {
"default": false,
"description": "Require platform utility resolution to stay within the configured path.",
"type": "boolean"
},
"version": {
"description": "Platform version requirement used for discovery.",
"type": [
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; strict resolution searches only the explicit boundary 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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

### Task 2: Typed strict locator

- [ ] Add failing locator tests for no fallback, exact/prefix mismatch, unknown version, and sibling consistency.
- [ ] Add typed policy/source/errors and source-aware candidates.
- [ ] Bind strict 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,27 @@
# Strict Platform Resolution Design

## Goal

Make an explicitly pinned 1C platform installation fail closed when requested, while preserving the legacy discovery fallback by default.

## Configuration contract

`tools.platform.strict` is a boolean with default `false`. `strict: true` requires `tools.platform.path`. The path is normalized relative to the primary config directory.

When strict mode is disabled, explicit path, default installation roots, and PATH keep the current fallback order. When strict mode is enabled, only the explicit path boundary is searched. A missing requested utility, an unknown version when `tools.platform.version` is configured, or a version mismatch is a typed locator error and never falls back.

Version requirements retain the existing semantics: four components are exact; two or three components are prefixes and select the highest matching installation below an explicit version root.

## Installation consistency

In strict mode, 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 in default roots or PATH is rejected.

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 strict missing paths, exact and prefix mismatch, unknown pinned version, versioned-root selection, sibling consistency, legacy fallback, relative path normalization, schema validation, and launch JSON metadata. Default behavior and existing locator tests remain unchanged.
76 changes: 76 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,77 @@ 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_rejects_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 error = load_config(config_path.to_str(), None).expect_err("strict path validation");

assert!(matches!(
error,
ConfigLoadError::ValidationError(ConfigValidationError::StrictPlatformRequiresPath)
));
}

#[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())
);
}
}
19 changes: 19 additions & 0 deletions src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,10 @@ pub struct PlatformToolConfig {
/// directory, or to a platform root that contains versioned subdirectories.
pub path: Option<PathBuf>,

/// Require platform utility resolution to stay within the configured path.
#[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 +655,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);
}
}
83 changes: 83 additions & 0 deletions src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,9 @@ struct PlatformToolSchema {
/// Platform binary, installation `bin` directory, or platform root discovery hint.
#[serde(default, skip_serializing_if = "Option::is_none")]
path: Option<PathBuf>,
/// Require platform utility resolution to stay within the configured path.
#[serde(default)]
strict: bool,
/// Platform version requirement used for discovery.
#[serde(default, skip_serializing_if = "Option::is_none")]
version: Option<String>,
Expand Down Expand Up @@ -1251,6 +1254,86 @@ mod tests {
assert_config_loader_ok(&config);
}

#[test]
fn platform_strict_main_schema_allows_path_to_come_from_local_overlay() {
let strict_without_path = format!(
"{}tools:\n platform:\n strict: true\n",
minimal_project_config_without_base_path()
);
let strict_with_null_path = format!(
"{}tools:\n platform:\n path: null\n strict: true\n",
minimal_project_config_without_base_path()
);
let non_strict_without_path = format!(
"{}tools:\n platform:\n strict: false\n",
minimal_project_config_without_base_path()
);

assert_schema_valid(&main_config_schema_json(), &strict_without_path);
assert_schema_valid(&main_config_schema_json(), &strict_with_null_path);
assert_schema_valid(&main_config_schema_json(), &non_strict_without_path);
}

#[test]
fn local_schema_accepts_strict_platform_override_when_main_config_supplies_path() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("Configuration.xml"), "<Configuration/>").expect("xml");
let config_path = dir.path().join("v8project.yaml");
std::fs::write(
&config_path,
format!(
"{}tools:\n platform:\n path: platform/bin\n",
minimal_project_config_without_base_path()
),
)
.expect("config");
let overlay = "tools:\n platform:\n strict: true\n";
std::fs::write(dir.path().join("v8project.local.yaml"), overlay).expect("overlay");

assert_schema_valid(&local_config_schema_json(), overlay);

let config = load_config(config_path.to_str(), None).expect("load merged config");
assert!(config.tools.platform.strict);
assert_eq!(
config.tools.platform.path.as_deref(),
Some(
std::fs::canonicalize(dir.path())
.expect("canonical config dir")
.join("platform/bin")
.as_path()
)
);
}

#[test]
fn local_schema_can_supply_path_for_strict_primary_platform_config() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("Configuration.xml"), "<Configuration/>").expect("xml");
let config_path = dir.path().join("v8project.yaml");
let primary = format!(
"{}tools:\n platform:\n strict: true\n",
minimal_project_config_without_base_path()
);
std::fs::write(&config_path, &primary).expect("config");
let overlay = "tools:\n platform:\n path: platform/bin\n";
std::fs::write(dir.path().join("v8project.local.yaml"), overlay).expect("overlay");

assert_schema_valid(&main_config_schema_json(), &primary);
assert_schema_valid(&local_config_schema_json(), overlay);

let config = load_config(config_path.to_str(), None).expect("load merged config");
assert!(config.tools.platform.strict);
assert_eq!(
config.tools.platform.path.as_deref(),
Some(
std::fs::canonicalize(dir.path())
.expect("canonical config dir")
.join("platform/bin")
.as_path()
)
);
}

#[test]
fn local_schema_and_loader_accept_canonical_mixed_config_keys() {
let overlay = "tools:\n enterprise:\n additional-launch-keys:\n - /TESTMANAGER\n edt_cli:\n startup_timeout_ms: 300000\n command_timeout_ms: 300000\n";
Expand Down
Loading