Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ history, authorship, licenses, and dependency notices.

## Current component

This source builds Web Console **1.6.198** and stdio bridge **0.2.0**. It adds
resource-family discovery, server-projected metadata and typed Stack, Service
and Container lifecycle plans, while retaining the seven existing tools. Browser
tools still start paused and require explicit opt-in. Pending adapters are
reported separately; this batch does not cover the whole console.

The [release note](docs/releases/web-console-1.6.198.md) records the scope and
This source builds Web Console **1.6.199** and stdio bridge **0.2.1**. Its twelve
typed tools provide resource discovery, safe metadata and human-approved workload
plans. Unsupported-operation replies now explain that Global services follow
eligible hosts and do not support a manual instance count. Browser tools still
start paused and require explicit opt-in. Pending adapters remain separate;
this batch does not cover the whole console.

The [release note](docs/releases/web-console-1.6.199.md) records the scope and
validation boundaries. Exact producer and immutable artifact identities are
recorded with the [release assets](https://github.com/PastureStack/web-console/releases).
Component publication and Server/browser acceptance are separate results.
Expand Down
3 changes: 3 additions & 0 deletions app/utils/ai-command-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export function publicError(error) {
if ( code === 'UNAVAILABLE' && status === 503 ) {
result.nextStep = 'Resource changes are disabled on this server. Ask an operator to enable the feature, or use the normal console.';
}
if ( code === 'UNAVAILABLE' && status === 422 && error && error.body && error.body.code === 'ResourceChangeUnsupported' ) {
result.nextStep = 'This operation is unsupported for this resource. Do not retry the same plan. If the request is service scale, Global services deploy according to eligible hosts and do not support a manual instance count. Review the service deployment configuration or eligible hosts in the console; do not guess another scale value.';
}
if ( code === 'CONFLICT' && error && error.body && error.body.code === 'ResourceChangeTargetBusy' ) {
result.reason = 'TARGET_BUSY';
result.nextStep = 'Wait for the current operation to finish. If you have its operationRef, call get_operation with it. Then read the resource again before previewing. Do not submit parallel changes.';
Expand Down
16 changes: 9 additions & 7 deletions bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ command layer. It needs a compatible Engine with `api.ai.bridge.enabled=true`,
a dedicated **AI collaboration bridge** Key and the parent repository's supported
Node.js toolchain. It cannot approve its own change plans.

The released 0.1.0 ZIP contains the initial seven-tool contract. The current
0.2.0 candidate adds resource-family discovery, safe metadata reads and typed
stack/service/container lifecycle plans. It requires a matching Engine with
The current 0.2.1 source contains twelve typed tools for resource-family
discovery, safe metadata reads and stack/service/container lifecycle plans.
Unsupported-operation replies explain that Global services follow eligible
hosts rather than a manual instance count; they do not advise retrying the plan.
It requires a matching Engine with
`api.ai.resource.change.enabled=true` for those changes (default false). Do not
expect these additions in the immutable 0.1.0 release archive. The generic
expect this reply correction in the immutable 0.2.0 release archive. The generic
stdio read adapter uses server-side safe views, never direct credential-bearing
domain resources. Pending adapters are reported by `list_capabilities`.

Expand All @@ -20,13 +22,13 @@ native browser support. The operator must separately opt in to
`api.ai.bridge.enabled` and, for upgrade plans, `api.ai.service.upgrade.enabled`;
both Engine flags default to false.

The candidate source archive is `pasturestack-webmcp-bridge-0.2.0.zip` with a
The source archive is `pasturestack-webmcp-bridge-0.2.1.zip` with a
`.zip.sha256` checksum. Extract the whole archive, keeping both `bridge/` and
`app/utils/`; the bridge uses those exact shared command sources. With Node.js
24.22.1 available, install from the extracted directory:

```sh
cd pasturestack-webmcp-bridge-0.2.0/bridge
cd pasturestack-webmcp-bridge-0.2.1/bridge
npm ci
```

Expand All @@ -35,7 +37,7 @@ records the included source hashes; the root also includes LICENSE and the
bridge CycloneDX SBOM. To reproduce the source release from a checkout:

```sh
python scripts/package-webmcp-bridge.py --version 0.2.0 --sbom /path/to/webmcp-bridge.cdx.json --output /path/to/pasturestack-webmcp-bridge-0.2.0.zip
python scripts/package-webmcp-bridge.py --version 0.2.1 --sbom /path/to/webmcp-bridge.cdx.json --output /path/to/pasturestack-webmcp-bridge-0.2.1.zip
```

Store the following configuration outside the extracted source tree in a private
Expand Down
4 changes: 2 additions & 2 deletions bridge/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bridge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/webmcp-bridge",
"version": "0.2.0",
"version": "0.2.1",
"private": true,
"type": "module",
"description": "Isolated stdio adapter for the shared PastureStack command contract",
Expand Down
2 changes: 1 addition & 1 deletion bridge/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ try {
const path = process.env.PASTURESTACK_BRIDGE_CONFIG;
if (!path) throw new SetupError('CONFIG_REQUIRED');
const bridge = await createBridge(await loadConfig(path));
const server = new Server({name: 'pasturestack-webmcp-bridge', version: '0.2.0'}, {
const server = new Server({name: 'pasturestack-webmcp-bridge', version: '0.2.1'}, {
capabilities: {tools: {}},
instructions: 'Use names and returned opaque references. Never guess IDs. A human must approve change plans in the console. Rejected calls must be repaired only as directed; never evade permissions. Accepted/running is not success. Read the existing operation until verifiedResult=true.',
});
Expand Down
1 change: 1 addition & 0 deletions bridge/test/manual-scale.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '../../scripts/test-webmcp-manual-scale.mjs';
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.198",
"version": "1.6.199",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.198",
"version": "1.6.199",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
24 changes: 24 additions & 0 deletions docs/releases/web-console-1.6.199.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Web Console 1.6.199

This source prepares Web Console 1.6.199 and the immutable stdio bridge 0.2.1.
Release CI, artifact verification and Server/browser acceptance are separate
gates. The published 1.6.198 and bridge 0.2.0 artifacts remain unchanged.

An authoritative `422 ResourceChangeUnsupported` still returns
`UNAVAILABLE / rejected`. Its tool reply now explains that Global services
deploy according to eligible hosts and do not support a manual instance count.
It directs the caller to review deployment configuration or eligible hosts,
without retrying the same plan or guessing a different count.

The matching Engine enforces manual-scale eligibility before preview or apply.
A target changed after preview retains the existing stale-snapshot conflict.
Permission failures, feature-off replies and uncertain transport outcomes keep
their existing categories. Global start/stop/restart and ordinary UI/API paths
are unchanged by this reply correction.

Three focused cases cover unsupported preview, an unsupported legacy plan at
apply, and the unchanged conflict/permission/feature-off/unknown boundaries.
No dependency graph changes, Catalog adapter, archive/restore work or new
translation keys are included. Existing twelve-tool, human approval and initial
off behavior remain. This source evidence does not claim the full frontend
inventory, role matrix or pending live candidate acceptance has passed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.198",
"version": "1.6.199",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.198" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.198"
if [[ "$version" != "1.6.199" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.199"
failures=$((failures + 1))
fi

Expand Down
4 changes: 3 additions & 1 deletion scripts/package-webmcp-bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import zipfile


VERSION = "0.2.0"
VERSION = "0.2.1"
SOURCE_FILES = (
"LICENSE",
"app/utils/ai-command-context.js",
Expand All @@ -25,9 +25,11 @@
"bridge/server.mjs",
"bridge/shared-modules.mjs",
"bridge/test/core.test.mjs",
"bridge/test/manual-scale.test.mjs",
"bridge/test/resources.test.mjs",
"bridge/test/stdio.test.mjs",
"scripts/test-webmcp-resources.mjs",
"scripts/test-webmcp-manual-scale.mjs",
)
FORBIDDEN_PARTS = {"node_modules", ".git", "credentials", "credential", "secrets"}

Expand Down
63 changes: 63 additions & 0 deletions scripts/test-webmcp-manual-scale.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {CommandContext, CommandEngine} from '../bridge/shared-modules.mjs';

const privateText = 'RAW_PRIVATE_SERVICE_CONFIG_MUST_NOT_LEAVE';
function fixture(error) {
const snapshot = {generation: 'g1', accountId: 'actor', projectId: 'project', schemaProjectId: 'project', schemaGeneration: 1, route: '/stacks'};
let sequence = 0;
const context = new CommandContext(() => snapshot, () => 'opaque-' + ++sequence); context.setMode('enabled');
const requests = [], audits = [];
const engine = new CommandEngine({context, ensureSession: async () => {},
audit: async event => { audits.push(event); return {accepted: true, requestId: 'audit-' + audits.length}; },
request: async request => { requests.push(request); throw error; }});
const captured = context.capture();
const targetRef = context.reference('resource', {id: 'service-one', family: 'service', resourceType: 'service', scope: 'project'}, captured);
const planRef = context.reference('plan', {id: 'aichange-11111111-1111-1111-1111-111111111111', planKind: 'resource-change',
resourceType: 'service', operation: 'scale', resourceId: 'service-one'}, captured);
return {engine, context, requests, audits, targetRef, planRef};
}
const unsupported = () => ({status: 422, body: {code: 'ResourceChangeUnsupported', message: privateText}});
function assertUnsupported(result) {
assert.equal(result.code, 'UNAVAILABLE'); assert.equal(result.status, 'rejected');
assert.match(result.nextStep, /Global services deploy according to eligible hosts/);
assert.match(result.nextStep, /do not support a manual instance count/);
assert.match(result.nextStep, /Do not retry the same plan/);
assert.match(result.nextStep, /do not guess another scale value/);
assert.equal(JSON.stringify(result).includes(privateText), false);
}

test('manual scale: authoritative unsupported preview remains rejected and explains Global eligible-host deployment', async () => {
const f = fixture(unsupported());
const result = await f.engine.execute('preview_resource_change', {resourceType: 'service', operation: 'scale', targetRef: f.targetRef, parameters: {scale: 2}});
assertUnsupported(result);
assert.equal(f.requests.length, 1);
assert.deepEqual(f.requests[0].data, {resourceType: 'service', operation: 'scale', resourceId: 'service-one', parameters: {scale: 2}});
assert.deepEqual(f.audits.map(event => [event.phase, event.status, event.reason]), [['start', 'pending', 'NONE'], ['result', 'rejected', 'UNAVAILABLE']]);
});

test('manual scale: an existing legacy Global plan rejected at apply is not an unknown write and is never retried', async () => {
const f = fixture(unsupported());
const result = await f.engine.execute('apply_resource_change', {planRef: f.planRef});
assertUnsupported(result);
assert.equal(f.requests.length, 1); assert.match(f.requests[0].url, /\?action=apply$/);
assert.deepEqual(f.requests[0].data, {});
const reference = f.context.resolve(f.planRef, 'plan', f.context.capture());
assert.equal(f.context.resolve(reference.operationRef, 'operation', f.context.capture()).lastStatus, 'rejected');
});

test('manual scale: changed-snapshot conflict, permission denial, feature-off and unknown transport retain their exact categories', async () => {
for (const [error, code, status] of [
[{status: 409, body: {code: 'ResourceChangePlanStale'}}, 'CONFLICT', 'rejected'],
[{status: 403, body: {code: 'ResourceChangeUnsupported'}}, 'FORBIDDEN', 'rejected'],
[{status: 503, body: {code: 'ResourceChangeFeatureDisabled'}}, 'UNAVAILABLE', 'rejected'],
[{status: 503, body: {code: 'ResourceChangeUnsupported'}}, 'UNKNOWN_RESULT', 'unknown'],
[new Error(privateText), 'UNKNOWN_RESULT', 'unknown'],
]) {
const f = fixture(error);
const result = await f.engine.execute('preview_resource_change', {resourceType: 'service', operation: 'scale', targetRef: f.targetRef, parameters: {scale: 2}});
assert.equal(result.code, code); assert.equal(result.status, status);
assert.equal(result.nextStep.includes('Global services'), false);
assert.equal(f.requests.length, 1); assert.equal(JSON.stringify(result).includes(privateText), false);
}
});
Loading