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
4 changes: 3 additions & 1 deletion app/utils/ai-resource-registry.js

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

2 changes: 1 addition & 1 deletion app/utils/ai-resource-review-labels.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const TYPES = {dnsService: 'servicePage.type.dnsservice', externalService: 'servicePage.type.externalservice', stack: 'apiKeyAccess.selector.types.stack', service: 'apiKeyAccess.selector.types.service', container: 'apiKeyAccess.selector.types.container'};
const TYPES = {certificate: 'certificatesPage.index.header', dnsService: 'servicePage.type.dnsservice', externalService: 'servicePage.type.externalservice', stack: 'apiKeyAccess.selector.types.stack', service: 'apiKeyAccess.selector.types.service', container: 'apiKeyAccess.selector.types.container'};
const OPERATIONS = {update_targets: 'formTarget.target', start: 'action.start', stop: 'action.stop', restart: 'action.restart', remove: 'action.remove',
create: 'saveCancel.create', create_from_catalog: 'newCatalog.saveNew', update_metadata: 'generic.edit', scale: 'serviceInfoPartial.scale', archive: 'archive.action', restore: 'archive.restore'};
const FIELDS = {targetServices: 'formTarget.target', hostname: 'formTarget.pointHostname', externalIpAddresses: 'formTarget.pointAddress', name: 'generic.name', description: 'generic.description', image: 'generic.image', scale: 'serviceInfoPartial.scale',
Expand Down
28 changes: 15 additions & 13 deletions bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 [published 0.2.3 archive](https://github.com/PastureStack/web-console/releases/tag/1.6.201)
The [0.2.4 release archive](https://github.com/PastureStack/web-console/releases/tag/1.6.202)
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
Expand All @@ -18,11 +18,13 @@ domain resources. Pending adapters are reported by `list_capabilities`.
External Services require an authorized Stack reference and exactly one target
mode (hostname or IPs). DNS Aliases are created stopped and empty; target binding
uses selected Service references in a separate approved plan. Typed audit queries
use the matching Engine 0.183.360 and broker owner, current Key scope and pinned
revision. Private audit payloads remain in the human UI. Web component and ZIP
qualification passed; matching Engine/Server and designated live workflows remain
pending. Publication does not update the existing Server `v1.6.559` / Web `1.6.200`
QA deployment or establish new-domain runtime acceptance.
use the matching Engine and broker owner, current Key scope and pinned revision.
Private audit payloads remain in the human UI. Certificate plans support only
name and description; PEM, private keys, chains, renewal and removal stay in the
normal human certificate UI. These metadata changes and the typed-domain audit
integration require Engine `0.183.361` or its matching Server release. A matching
version does not establish workflow acceptance; use the release's recorded
validation results and documented operation limits.

The Catalog source adapter also needs a matching Engine/Catalog typed metadata
endpoint. Use `find_resources` with `family: "catalog"` and
Expand All @@ -46,22 +48,22 @@ 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.

Download the numeric release's `pasturestack-webmcp-bridge-0.2.3.zip` and its
Download the numeric release's `pasturestack-webmcp-bridge-0.2.4.zip` and its
`.zip.sha256` checksum, then verify before extraction:

```sh
curl --fail --location --remote-name https://github.com/PastureStack/web-console/releases/download/1.6.201/pasturestack-webmcp-bridge-0.2.3.zip
curl --fail --location --remote-name https://github.com/PastureStack/web-console/releases/download/1.6.201/pasturestack-webmcp-bridge-0.2.3.zip.sha256
sha256sum --check pasturestack-webmcp-bridge-0.2.3.zip.sha256
unzip pasturestack-webmcp-bridge-0.2.3.zip
curl --fail --location --remote-name https://github.com/PastureStack/web-console/releases/download/1.6.202/pasturestack-webmcp-bridge-0.2.4.zip
curl --fail --location --remote-name https://github.com/PastureStack/web-console/releases/download/1.6.202/pasturestack-webmcp-bridge-0.2.4.zip.sha256
sha256sum --check pasturestack-webmcp-bridge-0.2.4.zip.sha256
unzip pasturestack-webmcp-bridge-0.2.4.zip
```

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.3/bridge
cd pasturestack-webmcp-bridge-0.2.4/bridge
npm ci --ignore-scripts --no-audit --no-fund
```

Expand All @@ -70,7 +72,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.3 --sbom /path/to/webmcp-bridge.cdx.json --output /path/to/pasturestack-webmcp-bridge-0.2.3.zip
python scripts/package-webmcp-bridge.py --version 0.2.4 --sbom /path/to/webmcp-bridge.cdx.json --output /path/to/pasturestack-webmcp-bridge-0.2.4.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.3",
"version": "0.2.4",
"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.3'}, {
const server = new Server({name: 'pasturestack-webmcp-bridge', version: '0.2.4'}, {
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
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.201",
"version": "1.6.202",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.201",
"version": "1.6.202",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
25 changes: 25 additions & 0 deletions docs/releases/web-console-1.6.202.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Web Console 1.6.202

Source candidate; publication and designated workflow acceptance are pending.

Certificate name and description can be changed through the existing resource
query, preview, normal human approval and execution flow. The optional stdio
bridge is 0.2.4. The public tool count remains twelve.

The operation uses a normal metadata-only certificate PUT. Certificate material,
private keys, chains, renewal and deletion remain in the normal human UI. Key
scope, current account permissions, ownership, revision and compare-and-set
checks apply to both preview and execution. Plans cannot approve themselves.

The matching Engine 0.183.361 must also support the certificate descriptor and
typed audit target. The same integration repairs External Service and DNS Alias
audit admission; disabling audit is not a workaround.

Validation covers the exact metadata wire body, private-data rejection and safe
projection, confirmation boundary and the registered Engine schema. Existing
unaffected acceptance keeps its original evidence. A successful build or one
completed workflow is not complete frontend acceptance.

Rollback requires the previous compatible Web/Engine/Server set. Let accepted
operations finish and cancel unused plans before changing versions; do not
mount an old JavaScript bundle over a newer image.
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.201",
"version": "1.6.202",
"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.201" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.201"
if [[ "$version" != "1.6.202" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.202"
failures=$((failures + 1))
fi

Expand Down
1 change: 1 addition & 0 deletions scripts/ci
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ node --test scripts/test-webmcp-path-boundaries.mjs
node --test scripts/test-ai-catalog-reader.mjs scripts/test-webmcp-catalog.mjs scripts/test-webmcp-catalog-launch.mjs scripts/test-webmcp-archive.mjs scripts/test-webmcp-resources.mjs
node --test scripts/test-webmcp-external-service.mjs
node --test scripts/test-webmcp-alias-service.mjs
node --test scripts/test-webmcp-certificate-metadata.mjs
node --test scripts/test-webmcp-audit.mjs scripts/test-webmcp-audit-extension.mjs
node scripts/test-ai-collaboration-ui.js
node scripts/test-ai-resource-review-labels.mjs
Expand Down
3 changes: 2 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.3"
VERSION = "0.2.4"
SOURCE_FILES = (
"LICENSE",
"app/utils/ai-command-context.js",
Expand Down Expand Up @@ -44,6 +44,7 @@
"scripts/test-webmcp-archive.mjs",
"scripts/test-webmcp-external-service.mjs",
"scripts/test-webmcp-alias-service.mjs",
"scripts/test-webmcp-certificate-metadata.mjs",
"scripts/test-webmcp-audit.mjs",
"scripts/test-webmcp-audit-extension.mjs",
)
Expand Down
86 changes: 86 additions & 0 deletions scripts/test-webmcp-certificate-metadata.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {COMMANDS, CommandContext, CommandEngine} from '../bridge/shared-modules.mjs';
import {readFileSync} from 'node:fs';
const {resourceReviewLabels} = await import('data:text/javascript;base64,' + readFileSync(new URL('../app/utils/ai-resource-review-labels.js', import.meta.url)).toString('base64'));

const id = 'aichange-11111111-1111-1111-1111-111111111111';
function fixture() {
const snapshot = {generation: 'g1', accountId: 'a1', projectId: 'p1', projectName: 'Environment', schemaProjectId: 'p1', schemaGeneration: 1, route: '/certificates'};
let sequence = 0, plan, dispatches = 0;
const context = new CommandContext(() => snapshot, () => String(++sequence)); context.setMode('enabled');
const requests = [], rows = ['first', 'selected'].map(suffix => ({id: `cert-${suffix}`, accountId: 'p1', name: 'same-name', description: suffix,
state: 'active', CN: `${suffix}.example.test`, cert: 'PRIVATE-CERT-CANARY', key: 'PRIVATE-KEY-CANARY', data: {key: 'PRIVATE-DATA-CANARY'}}));
const engine = new CommandEngine({context, transportKind: 'stdio', ensureSession: async () => {}, wait: async () => {},
audit: async () => ({accepted: true, requestId: `audit${++sequence}`}), request: async request => {
requests.push(request);
if (request.url === 'projects/p1/aiResourceViews') return {body: {resourceType: 'certificate',
data: request.data.resourceId ? rows.filter(row => row.id === request.data.resourceId) : rows}};
if (request.url === 'projects/p1/aiResourceChangePlans') {
const {resourceType, operation, resourceId, parameters} = request.data;
plan = {id, resourceType, operation, resourceId, targetPath: 'Environment / same-name',
changes: Object.entries(parameters).map(([field, after]) => ({field, before: field === 'name' ? 'same-name' : 'selected', after})),
confirmationState: 'pending', status: 'preview', verifiedResult: false, expiresAt: '2026-11-01T00:00:00Z'};
return {body: {...plan}};
}
if (request.url.endsWith('?action=confirm')) { plan.confirmationState = 'approved'; return {body: {...plan}}; }
if (request.url.endsWith('?action=apply')) {
if (plan.confirmationState !== 'approved') throw {status: 409, body: {code: 'ResourceChangeNeedsConfirmation'}};
dispatches++; plan.status = 'accepted'; return {body: {...plan}};
}
if (request.url.endsWith(id)) return {body: {...plan}};
throw Error('Unexpected fixed route');
}});
return {engine, context, snapshot, requests, rows, plan: () => plan, dispatches: () => dispatches};
}

test('certificate: selected same-name opaque reference -> safe metadata -> human approved plan -> verified receipt', async () => {
const f = fixture(), found = await f.engine.execute('find_resources', {family: 'certificates', query: 'same-name'});
assert.equal(found.items.length, 2); assert.notEqual(found.items[0].resourceRef, found.items[1].resourceRef);
const targetRef = found.items[1].resourceRef;
const detail = await f.engine.execute('get_resource', {resourceRef: targetRef});
assert.equal(detail.fields.CN, 'selected.example.test'); assert.equal(JSON.stringify([found, detail]).includes('PRIVATE-'), false);
const preview = await f.engine.execute('preview_resource_change', {resourceType: 'certificate', operation: 'update_metadata', targetRef,
parameters: {name: 'renamed', description: ''}});
assert.equal(preview.status, 'needs_user'); assert.equal(f.requests.at(-1).data.resourceId, 'cert-selected');
assert.equal(JSON.stringify(preview).includes('cert-selected'), false);
assert.equal((await f.engine.execute('apply_resource_change', {planRef: preview.planRef})).code, 'NEEDS_USER');
assert.equal(f.dispatches(), 0); await f.engine.confirmPlan(preview.planRef);
const accepted = await f.engine.execute('apply_resource_change', {planRef: preview.planRef});
assert.equal(accepted.status, 'accepted'); assert.equal(f.dispatches(), 1);
f.plan().status = 'succeeded'; f.plan().verifiedResult = true;
const observed = await f.engine.execute('get_operation', {operationRef: accepted.operationRef});
assert.equal(observed.status, 'succeeded'); assert.equal(observed.verifiedResult, true);
assert.ok(f.requests.every(request => request.method !== 'PUT' && !request.url.includes('/certificates')));
});

test('certificate: material, raw IDs and unsupported intents never reach transport', async () => {
const f = fixture(), found = await f.engine.execute('find_resources', {family: 'certificates'});
const input = {resourceType: 'certificate', operation: 'update_metadata', targetRef: found.items[0].resourceRef};
const before = f.requests.length;
for (const field of ['cert', 'key', 'certChain', 'data', 'accountId', 'uuid', 'state']) {
assert.equal((await f.engine.execute('preview_resource_change', {...input, parameters: {[field]: 'PRIVATE-CANARY'}})).code, 'INVALID_INPUT');
}
for (const operation of ['create', 'remove', 'start', 'stop', 'update_targets']) {
assert.equal((await f.engine.execute('preview_resource_change', {...input, operation, parameters: {name: 'x'}})).code, 'INVALID_INPUT');
}
assert.equal((await f.engine.execute('preview_resource_change', {...input, targetRef: 'cert-first', parameters: {name: 'x'}})).code, 'STALE_REFERENCE');
assert.equal(f.requests.length, before);
});

test('certificate: project changes invalidate issued references before preview', async () => {
const f = fixture(), found = await f.engine.execute('find_resources', {family: 'certificates'});
const before = f.requests.length; f.snapshot.projectId = 'foreign'; f.snapshot.schemaProjectId = 'foreign';
const rejected = await f.engine.execute('preview_resource_change', {resourceType: 'certificate', operation: 'update_metadata',
targetRef: found.items[0].resourceRef, parameters: {name: 'new'}});
assert.ok(['CONTEXT_CHANGED', 'STALE_REFERENCE'].includes(rejected.code)); assert.equal(f.requests.length, before);
});

test('certificate: capabilities, closed input enum and human review expose only metadata changes', async () => {
const f = fixture(), capability = (await f.engine.execute('list_capabilities', {})).families.find(family => family.family === 'certificates').resources[0];
assert.deepEqual(capability.operations, ['update_metadata']);
const tool = COMMANDS.find(tool => tool.name === 'preview_resource_change');
assert.ok(tool.inputSchema.properties.resourceType.enum.includes('certificate'));
const labels = resourceReviewLabels({resourceType: 'certificate', operation: 'update_metadata', displayDiff: [{field: 'description', before: null, after: ''}]}, key => key);
assert.equal(labels.resourceTypeLabel, 'certificatesPage.index.header'); assert.equal(labels.operationLabel, 'generic.edit');
});
Loading
Loading