From 5d30b122da610920589a016e260d3afbeebabb05 Mon Sep 17 00:00:00 2001
From: chen21019 <19357113+chen21019@users.noreply.github.com>
Date: Thu, 29 Oct 2026 23:48:16 +0800
Subject: [PATCH 1/3] Add typed AI resource discovery and human-reviewed
workload plans
---
README.md | 22 +-
app/components/ai-upgrade-review/template.hbs | 2 +-
.../modal-ai-collaboration/component.js | 61 ++-
.../modal-ai-collaboration/template.hbs | 14 +-
.../modal-confirm-ai-upgrade/component.js | 10 +-
.../modal-confirm-ai-upgrade/template.hbs | 20 +-
app/services/ai-command-service.js | 59 ++-
app/styles/components/_modals.scss | 11 +
app/utils/ai-command-contract.js | 84 +++-
app/utils/ai-command-engine.js | 334 ++++++++++++-
app/utils/ai-resource-registry.js | 84 ++++
app/utils/ai-resource-review-labels.js | 35 ++
app/utils/api-key-policy-matrix.js | 13 +-
bridge/README.md | 24 +-
bridge/core.mjs | 23 +-
bridge/package-lock.json | 4 +-
bridge/package.json | 2 +-
bridge/server.mjs | 8 +-
bridge/shared-modules.mjs | 4 +-
bridge/test/core.test.mjs | 4 +-
bridge/test/resources.test.mjs | 2 +
bridge/test/stdio.test.mjs | 55 ++-
...ass-replacement.node24-ignore-scripts.json | 4 +-
docs/releases/web-console-1.6.198.md | 37 ++
docs/webmcp.md | 61 ++-
package-lock.json | 4 +-
package.json | 2 +-
scripts/check-modernization-blockers | 4 +-
scripts/check-webmcp-engine-contract.mjs | 19 +-
scripts/package-webmcp-bridge.py | 5 +-
scripts/test-ai-bridge-policy-matrix.js | 70 +++
scripts/test-ai-collaboration-ui.js | 98 +++-
scripts/test-ai-resource-review-labels.mjs | 71 +++
scripts/test-webmcp-commands.js | 7 +-
scripts/test-webmcp-path-boundaries.mjs | 35 +-
scripts/test-webmcp-resources.mjs | 437 ++++++++++++++++++
.../unit/utils/api-key-policy-matrix-test.js | 8 +-
translations/de-de.yaml | 28 +-
translations/en-us.yaml | 28 +-
translations/fa-ir.yaml | 28 +-
translations/fil-ph.yaml | 28 +-
translations/fr-fr.yaml | 28 +-
translations/hu-hu.yaml | 28 +-
translations/ja-jp.yaml | 28 +-
translations/ko-kr.yaml | 28 +-
translations/pt-br.yaml | 28 +-
translations/ru-ru.yaml | 28 +-
translations/uk-ua.yaml | 28 +-
translations/zh-hans.yaml | 28 +-
translations/zh-tw.yaml | 28 +-
50 files changed, 1928 insertions(+), 173 deletions(-)
create mode 100644 app/utils/ai-resource-registry.js
create mode 100644 app/utils/ai-resource-review-labels.js
create mode 100644 bridge/test/resources.test.mjs
create mode 100644 docs/releases/web-console-1.6.198.md
create mode 100644 scripts/test-ai-bridge-policy-matrix.js
create mode 100644 scripts/test-ai-resource-review-labels.mjs
create mode 100644 scripts/test-webmcp-resources.mjs
diff --git a/README.md b/README.md
index 465a3a44f3..8cf0a10d66 100644
--- a/README.md
+++ b/README.md
@@ -10,14 +10,16 @@ history, authorship, licenses, and dependency notices.
## Current component
-Web Console [1.6.197](https://github.com/PastureStack/web-console/releases/tag/1.6.197)
-restores the verified browser actor when an existing cookie is opened in a
-fresh browser session. This allows the human approval inbox to work without
-requiring a new login. Optional AI collaboration still provides seven typed
-tools, conditional description edits and a dedicated-Key stdio bridge.
-Browser tools start paused and require an explicit user choice. See the
-[release note](docs/releases/web-console-1.6.197.md) for the session boundary,
-original producer and verified publication.
+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
+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.
The release producer uses Node.js `24.22.1` / npm `12.0.2` in a digest-pinned
container and produces an immutable static archive with separate runtime and
@@ -30,8 +32,8 @@ for deployment. Historical changes remain in [release notes](docs/releases).
## Features and operator guides
-- [Experimental AI collaboration](docs/webmcp.md): seven typed tools for
- discovery, conditional description edits and server-owned upgrade plans.
+- [Experimental AI collaboration](docs/webmcp.md): twelve typed tools for
+ resource discovery, conditional descriptions and server-owned lifecycle plans.
Disabled by default. Includes human review controls and an optional
[dedicated-Key stdio bridge](bridge/README.md); native browser tools require
a supported secure-context browser.
diff --git a/app/components/ai-upgrade-review/template.hbs b/app/components/ai-upgrade-review/template.hbs
index 01fbcd40a3..26951dd546 100644
--- a/app/components/ai-upgrade-review/template.hbs
+++ b/app/components/ai-upgrade-review/template.hbs
@@ -1,6 +1,6 @@
{{#if this.commands.pendingPlan}}
{{this.commands.pendingPlan.displayPath}}
-
+
{{/if}}
diff --git a/app/components/modal-ai-collaboration/component.js b/app/components/modal-ai-collaboration/component.js
index 3e8149ddd8..903c9fe573 100644
--- a/app/components/modal-ai-collaboration/component.js
+++ b/app/components/modal-ai-collaboration/component.js
@@ -3,6 +3,7 @@ import { service } from '@ember/service';
import ModalBase from 'lacsso/components/modal-base';
import config from 'ui/config/environment';
import { boundedCollaborationMode, collaborationStatusKey } from 'ui/utils/ai-collaboration-controls';
+import { resourceReviewLabels } from 'ui/utils/ai-resource-review-labels';
export default ModalBase.extend({
classNames: ['lacsso', 'modal-container', 'medium-modal'],
@@ -23,6 +24,10 @@ export default ModalBase.extend({
loadError: false,
hasLoaded: false,
plans: null,
+ cancelCandidate: null,
+ cancelling: false,
+ cancelNotice: null,
+ errors: null,
canEnable: computed('buildMode', function() { return this.get('buildMode') === 'enabled'; }),
statusKey: computed('registry.status', function() { return collaborationStatusKey(this.get('registry.status')); }),
@@ -40,7 +45,7 @@ export default ModalBase.extend({
invalidateInbox() {
this._loadGeneration++;
- if ( !this.isDestroying && !this.isDestroyed ) { this.setProperties({plans: [], loading: false, hasLoaded: false, loadError: false}); }
+ if ( !this.isDestroying && !this.isDestroyed ) { this.setProperties({plans: [], loading: false, hasLoaded: false, loadError: false, cancelCandidate: null, cancelling: false, cancelNotice: null, errors: null}); }
},
currentLoad(generation, captured) {
@@ -51,23 +56,29 @@ export default ModalBase.extend({
async loadPlans() {
const generation = ++this._loadGeneration;
- this.setProperties({loading: true, loadError: false, plans: [], hasLoaded: false});
+ this.setProperties({loading: true, loadError: false, plans: [], hasLoaded: false, cancelCandidate: null, cancelNotice: null, errors: null});
let captured;
try {
captured = this.get('commands').captureReviewContext();
await this.get('access').ensureSession();
if ( !this.currentLoad(generation, captured) ) { return; }
- const response = await this.get('userStore').rawRequest({
- url: `projects/${encodeURIComponent(captured.projectId)}/aiServiceUpgradePlans?limit=20`, method: 'GET',
- });
+ const responses = await Promise.all(['aiServiceUpgradePlans', 'aiResourceChangePlans'].map((collection) =>
+ this.get('userStore').rawRequest({url: `projects/${encodeURIComponent(captured.projectId)}/${collection}?limit=20`, method: 'GET'})));
if ( !this.currentLoad(generation, captured) ) { return; }
- if ( !response.body || !Array.isArray(response.body.data) || response.body.data.length > 20 ) { throw new Error('InvalidInbox'); }
- const plans = response.body.data.map((plan) => {
- if ( !/^aiplan-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(plan.id || '') || typeof plan.serviceId !== 'string' ) {
- throw new Error('InvalidInboxPlan');
- }
- const safe = this.get('commands').engine.planSummary(plan, null);
- return Object.assign({id: plan.id, serviceId: plan.serviceId}, safe);
+ const plans = responses.flatMap((response, index) => {
+ if ( !response.body || !Array.isArray(response.body.data) || response.body.data.length > 20 ) { throw new Error('InvalidInbox'); }
+ return response.body.data.map((plan) => {
+ const resourceChange = index === 1;
+ if ( !(resourceChange ? /^aichange-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i : /^aiplan-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i).test(plan.id || '') ||
+ !resourceChange && typeof plan.serviceId !== 'string' ) {
+ throw new Error('InvalidInboxPlan');
+ }
+ const safe = this.get('commands').engine.planSummary(plan, null);
+ const typed = resourceChange ? {targetPath: safe.displayPath, changes: safe.displayDiff,
+ ...resourceReviewLabels(safe, (key) => this.get('intl').t(key))} : {};
+ return Object.assign({id: plan.id, serviceId: plan.serviceId, resourceId: plan.resourceId, status: plan.status, operationId: plan.operationId,
+ canCancel: this.get('commands').canCancelInboxPlan(plan)}, safe, typed);
+ });
});
this._loadedContext = captured;
this.setProperties({plans, hasLoaded: true});
@@ -91,6 +102,32 @@ export default ModalBase.extend({
finally { if ( !this.isDestroying && !this.isDestroyed ) { this.set('changing', false); } }
},
refresh() { return this.loadPlans(); },
+ requestCancel(plan) {
+ if ( this.get('cancelling') || !plan.canCancel ) { return; }
+ this.setProperties({cancelCandidate: plan, errors: null, cancelNotice: null});
+ },
+ keepPlan() { if ( !this.get('cancelling') ) { this.set('cancelCandidate', null); } },
+ async cancelPlan() {
+ const plan = this.get('cancelCandidate'), generation = this._loadGeneration, captured = this._loadedContext;
+ if ( !plan || this.get('cancelling') || !this.get('plans').includes(plan) ) { return; }
+ this.setProperties({cancelling: true, errors: null});
+ try {
+ const result = await this.get('commands').cancelInboxPlan(plan, captured);
+ if ( !this.currentLoad(generation, captured) ) { return; }
+ if ( result.status === 'succeeded' && result.confirmationState === 'cancelled' ) {
+ this.setProperties({plans: this.get('plans').filter((entry) => entry.id !== plan.id), cancelCandidate: null, cancelNotice: this.get('intl').t('webmcp.planCancelled')});
+ } else {
+ if ( result.reason === 'CANNOT_CANCEL' ) {
+ this.setProperties({plans: this.get('plans').map((entry) => entry.id === plan.id ? Object.assign({}, entry, {canCancel: false}) : entry), cancelCandidate: null});
+ }
+ this.set('errors', [this.get('intl').t(result.reason === 'CANNOT_CANCEL' ? 'webmcp.cannotCancel' : 'webmcp.cancelFailed')]);
+ }
+ } catch (error) {
+ if ( !this.isDestroying && !this.isDestroyed && generation === this._loadGeneration ) { this.set('errors', [this.get('intl').t('webmcp.cancelFailed')]); }
+ } finally {
+ if ( !this.isDestroying && !this.isDestroyed && generation === this._loadGeneration ) { this.set('cancelling', false); }
+ }
+ },
review(plan) {
if ( this.isDestroying || this.isDestroyed ) { return; }
const result = this.get('commands').reviewInboxPlan(plan, this._loadedContext);
diff --git a/app/components/modal-ai-collaboration/template.hbs b/app/components/modal-ai-collaboration/template.hbs
index a6e144968c..1e0c65f92f 100644
--- a/app/components/modal-ai-collaboration/template.hbs
+++ b/app/components/modal-ai-collaboration/template.hbs
@@ -15,14 +15,22 @@
{{t "webmcp.collaboration.pendingPlans"}}
{{t "webmcp.collaboration.confirmationSource"}}
+{{top-errors errors=this.errors}}
+{{#if this.cancelNotice}}{{this.cancelNotice}}
{{/if}}
+{{#if this.cancelCandidate}}
+
{{t "webmcp.cancelPlanPrompt"}}
{{this.cancelCandidate.displayPath}}
+
+
+
{{/if}}
{{#if this.loading}}{{t "generic.loading"}}
{{else if this.loadError}}{{t "webmcp.collaboration.loadFailed"}}
{{else if this.hasLoaded}}
- {{#each this.plans as |plan|}}-
+ {{#each this.plans as |plan|}}
-
{{plan.displayPath}}
-
{{plan.currentImage}} → {{plan.requestedImage}}
-
+ {{#if (eq plan.planKind "resource-change")}}{{plan.resourceTypeLabel}} · {{plan.operationLabel}}
{{else}}{{plan.currentImage}} → {{plan.requestedImage}}
{{/if}}
+
+ {{#if plan.canCancel}}{{/if}}
{{else}}- {{t "webmcp.collaboration.noPendingPlans"}}
{{/each}}
{{else}}{{t "webmcp.collaboration.loadRequired"}}
{{/if}}
diff --git a/app/components/modal-confirm-ai-upgrade/component.js b/app/components/modal-confirm-ai-upgrade/component.js
index c6b9bbae04..eabae5a345 100644
--- a/app/components/modal-confirm-ai-upgrade/component.js
+++ b/app/components/modal-confirm-ai-upgrade/component.js
@@ -2,6 +2,7 @@ import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { service } from '@ember/service';
import ModalBase from 'lacsso/components/modal-base';
+import { resourceReviewLabels } from 'ui/utils/ai-resource-review-labels';
export default ModalBase.extend({
classNames: ['lacsso', 'modal-container', 'medium-modal'],
@@ -14,7 +15,12 @@ export default ModalBase.extend({
plan: alias('modalService.modalOpts.plan'),
saving: false,
approved: false,
+ alreadyApproved: computed('approved', 'plan.confirmationState', function() { return this.get('approved') || this.get('plan.confirmationState') === 'approved'; }),
errors: null,
+ isResourceChange: computed('plan.planKind', function() { return this.get('plan.planKind') === 'resource-change'; }),
+ reviewLabels: computed('plan.displayDiff.[]', 'plan.resourceType', 'plan.operation', 'intl.primaryLocale', function() {
+ return resourceReviewLabels(this.get('plan') || {}, (key) => this.get('intl').t(key));
+ }),
displayExpiry: computed('plan.expiresAt', 'intl.primaryLocale', function() {
const value = new Date(this.get('plan.expiresAt'));
if (!Number.isFinite(value.getTime())) { return ''; }
@@ -25,8 +31,8 @@ export default ModalBase.extend({
stale: computed('plan.planRef', 'commands.pendingPlan.planRef', function() {
return !this.get('plan.planRef') || this.get('plan.planRef') !== this.get('commands.pendingPlan.planRef');
}),
- submitDisabled: computed('saving', 'approved', 'stale', function() {
- return this.get('saving') || this.get('approved') || this.get('stale');
+ submitDisabled: computed('saving', 'alreadyApproved', 'stale', function() {
+ return this.get('saving') || this.get('alreadyApproved') || this.get('stale');
}),
didInsertElement() {
diff --git a/app/components/modal-confirm-ai-upgrade/template.hbs b/app/components/modal-confirm-ai-upgrade/template.hbs
index af1c112360..16ce8958fd 100644
--- a/app/components/modal-confirm-ai-upgrade/template.hbs
+++ b/app/components/modal-confirm-ai-upgrade/template.hbs
@@ -1,13 +1,21 @@
-{{t "webmcp.title"}}
+{{#if this.isResourceChange}}{{t "webmcp.resourceChangeTitle" operation=this.reviewLabels.operationLabel resource=this.reviewLabels.resourceTypeLabel}}{{else}}{{t "webmcp.title"}}{{/if}}
- - {{t "webmcp.service"}}
- {{this.plan.displayPath}}
- - {{t "webmcp.currentImage"}}
- {{this.plan.currentImage}}
- - {{t "webmcp.requestedImage"}}
- {{this.plan.requestedImage}}
+ - {{#if this.isResourceChange}}{{t "generic.name"}}{{else}}{{t "webmcp.service"}}{{/if}}
- {{this.plan.displayPath}}
+ {{#if this.isResourceChange}}
+ - {{t "generic.type"}}
- {{this.reviewLabels.resourceTypeLabel}}
+ - {{t "generic.actions"}}
- {{this.reviewLabels.operationLabel}}
+ {{#each this.reviewLabels.displayChanges as |change|}}- {{change.fieldLabel}}
- {{#unless change.informational}}
{{change.before}}→{{/unless}}{{change.after}} {{/each}}
+ {{else}}
+ - {{t "webmcp.currentImage"}}
- {{this.plan.currentImage}}
+ - {{t "webmcp.requestedImage"}}
- {{this.plan.requestedImage}}
+ {{/if}}
- {{t "webmcp.expires"}}
- {{this.displayExpiry}}
-{{t "webmcp.notice"}}
+{{#if this.isResourceChange}}{{t "webmcp.resourceChangeNotice"}}{{else}}{{t "webmcp.notice"}}{{/if}}
+{{#if this.reviewLabels.createsStopped}}{{t "webmcp.createStoppedNotice"}}
{{/if}}
+{{#if this.reviewLabels.mountedDataNotice}}{{this.reviewLabels.mountedDataNotice}}
{{/if}}
{{#if this.stale}}{{t "webmcp.stale"}}
{{/if}}
-{{#if this.approved}}{{t "webmcp.approved"}}
{{/if}}
+{{#if this.alreadyApproved}}{{t "webmcp.approved"}}
{{/if}}
{{top-errors errors=this.errors}}