@honua/mcp-server 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/certification/geospatial-mcp-schemas/PROVENANCE.md +14 -8
- package/certification/geospatial-mcp-schemas/common/controls.schema.json +81 -0
- package/certification/geospatial-mcp-schemas/common/exploration.schema.json +165 -0
- package/certification/geospatial-mcp-schemas/common/interactions.schema.json +263 -0
- package/certification/geospatial-mcp-schemas/index.json +155 -4
- package/certification/geospatial-mcp-schemas/resources/app-package.schema.json +17 -1
- package/certification/geospatial-mcp-schemas/resources/map-package.schema.json +17 -1
- package/certification/geospatial-mcp-schemas/resources/ops-findings.schema.json +61 -0
- package/certification/geospatial-mcp-schemas/resources/ops-health.schema.json +138 -0
- package/certification/geospatial-mcp-schemas/tools/add_control.schema.json +107 -0
- package/certification/geospatial-mcp-schemas/tools/alert_events.result.schema.json +49 -0
- package/certification/geospatial-mcp-schemas/tools/alert_events.schema.json +49 -0
- package/certification/geospatial-mcp-schemas/tools/bind_component_context.schema.json +60 -0
- package/certification/geospatial-mcp-schemas/tools/bind_interaction.schema.json +97 -0
- package/certification/geospatial-mcp-schemas/tools/create_exploration_context.schema.json +35 -0
- package/certification/geospatial-mcp-schemas/tools/deploy_operations.result.schema.json +109 -0
- package/certification/geospatial-mcp-schemas/tools/deploy_operations.schema.json +49 -0
- package/certification/geospatial-mcp-schemas/tools/operate_events.result.schema.json +67 -0
- package/certification/geospatial-mcp-schemas/tools/operate_events.schema.json +46 -0
- package/certification/geospatial-mcp-schemas/tools/ops_findings.schema.json +25 -0
- package/certification/geospatial-mcp-schemas/tools/ops_health.schema.json +10 -0
- package/certification/geospatial-mcp-schemas/tools/platform_release_status.result.schema.json +53 -0
- package/certification/geospatial-mcp-schemas/tools/platform_release_status.schema.json +10 -0
- package/certification/geospatial-mcp-schemas/tools/propose_operation.schema.json +7 -2
- package/certification/geospatial-mcp-schemas/tools/propose_rollback.schema.json +29 -0
- package/certification/geospatial-mcp-schemas/tools/remove_control.schema.json +114 -0
- package/certification/geospatial-mcp-schemas/tools/remove_exploration_context.schema.json +42 -0
- package/certification/geospatial-mcp-schemas/tools/remove_interaction.schema.json +99 -0
- package/dist/src/certification/certifier.d.ts +17 -2
- package/dist/src/certification/certifier.d.ts.map +1 -1
- package/dist/src/certification/certifier.js +69 -11
- package/dist/src/certification/cli.js +4 -1
- package/dist/src/certification/lifecycle.d.ts +2 -0
- package/dist/src/certification/lifecycle.d.ts.map +1 -1
- package/dist/src/certification/lifecycle.js +3 -0
- package/dist/src/certification/operator-catalog.d.ts.map +1 -1
- package/dist/src/certification/operator-catalog.js +0 -12
- package/dist/src/certification/report.d.ts.map +1 -1
- package/dist/src/certification/report.js +2 -1
- package/dist/src/certification/roster.d.ts +99 -0
- package/dist/src/certification/roster.d.ts.map +1 -0
- package/dist/src/certification/roster.js +233 -0
- package/dist/src/certification/round-trip-target.d.ts +45 -0
- package/dist/src/certification/round-trip-target.d.ts.map +1 -0
- package/dist/src/certification/round-trip-target.js +122 -0
- package/dist/src/certification/run.d.ts.map +1 -1
- package/dist/src/certification/run.js +7 -0
- package/dist/src/eval/northstar-corpus.d.ts.map +1 -1
- package/dist/src/eval/northstar-corpus.js +12 -10
- package/dist/src/eval/operator-corpus.d.ts.map +1 -1
- package/dist/src/eval/operator-corpus.js +11 -6
- package/dist/src/eval/proposal-tools.d.ts +14 -0
- package/dist/src/eval/proposal-tools.d.ts.map +1 -0
- package/dist/src/eval/proposal-tools.js +20 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -6
- package/dist/src/neutral/query.js +1 -1
- package/dist/src/neutral/result.d.ts +2 -2
- package/dist/src/neutral/result.d.ts.map +1 -1
- package/dist/src/neutral/result.js +11 -4
- package/dist/src/release/zero-to-map-cli.d.ts.map +1 -1
- package/dist/src/release/zero-to-map-cli.js +8 -2
- package/dist/src/release/zero-to-map-provision.d.ts +6 -0
- package/dist/src/release/zero-to-map-provision.d.ts.map +1 -1
- package/dist/src/release/zero-to-map-provision.js +6 -0
- package/dist/src/tools/apply-style-preset.d.ts +1 -1
- package/dist/src/tools/apply-style-preset.d.ts.map +1 -1
- package/dist/src/tools/apply-style-preset.js +4 -2
- package/dist/src/tools/control-plane-command.d.ts +1 -1
- package/dist/src/tools/control-plane-command.d.ts.map +1 -1
- package/dist/src/tools/control-plane-command.js +3 -2
- package/dist/src/tools/count-features.d.ts +1 -1
- package/dist/src/tools/count-features.d.ts.map +1 -1
- package/dist/src/tools/count-features.js +2 -1
- package/dist/src/tools/docs-search.d.ts +2 -2
- package/dist/src/tools/docs-search.d.ts.map +1 -1
- package/dist/src/tools/docs-search.js +15 -4
- package/dist/src/tools/get-extent.d.ts +1 -1
- package/dist/src/tools/get-extent.d.ts.map +1 -1
- package/dist/src/tools/get-extent.js +2 -1
- package/dist/src/tools/query-features.d.ts +11 -2
- package/dist/src/tools/query-features.d.ts.map +1 -1
- package/dist/src/tools/query-features.js +18 -5
- package/dist/src/tools/statistics.d.ts +1 -1
- package/dist/src/tools/statistics.d.ts.map +1 -1
- package/dist/src/tools/statistics.js +2 -1
- package/package.json +2 -2
- package/release/zero-to-map/configs/claude-code.mcp.json +1 -1
- package/release/zero-to-map/configs/claude-desktop.json +1 -1
- package/release/zero-to-map/configs/cursor.json +1 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* honua-server MCP tool roster (`docs/gis/data/mcp-tool-roster.v1.json`) and the
|
|
3
|
+
* `roster-parity` check the certifier runs against a live `/mcp`.
|
|
4
|
+
*
|
|
5
|
+
* The roster is the server's own generated list of every tool name its composed
|
|
6
|
+
* registry can advertise. It is the authority this repo's catalogs and corpora
|
|
7
|
+
* are checked against, so a retired tool (e.g. `honua_propose_operation`) can no
|
|
8
|
+
* longer linger here after the server drops it.
|
|
9
|
+
*
|
|
10
|
+
* The roster is read BY SERVER SHA: the same honua-server commit the pinned
|
|
11
|
+
* candidate image was built from (`ADMIN_LOCAL_SERVER_SHA` in the installer lane). When the file does
|
|
12
|
+
* not exist at that sha (the server change that adds it has not reached the pin
|
|
13
|
+
* yet), the check reports `blocked` with the reason. It never passes without a
|
|
14
|
+
* roster to compare against.
|
|
15
|
+
*/
|
|
16
|
+
/** Path of the roster inside the honua-server repository. */
|
|
17
|
+
export declare const ROSTER_REPO_PATH = "docs/gis/data/mcp-tool-roster.v1.json";
|
|
18
|
+
/** Raw-content URL of the roster at a honua-server commit. */
|
|
19
|
+
export declare function rosterUrl(serverSha: string): string;
|
|
20
|
+
export type RosterViewName = "default" | "setup" | "configure" | "operate" | "analyze";
|
|
21
|
+
export declare const ROSTER_VIEW_NAMES: readonly RosterViewName[];
|
|
22
|
+
/** Shape of `mcp-tool-roster.v1.json` (fields this repo consumes). */
|
|
23
|
+
export interface McpToolRoster {
|
|
24
|
+
serverSha: string | null;
|
|
25
|
+
/** Hand-authored IMcpTool names, every capability gate enabled. */
|
|
26
|
+
static: string[];
|
|
27
|
+
/** `honua_admin_*` tools the default production composition publishes. */
|
|
28
|
+
projectedAdmin: string[];
|
|
29
|
+
/** Subset of projectedAdmin that registers only with a durable proposal store (Redis). */
|
|
30
|
+
requiresDurableControlPlane: string[];
|
|
31
|
+
views: Record<RosterViewName, string[]>;
|
|
32
|
+
/** Names the server no longer advertises. */
|
|
33
|
+
retired: string[];
|
|
34
|
+
}
|
|
35
|
+
export type RosterResolution = {
|
|
36
|
+
status: "loaded";
|
|
37
|
+
roster: McpToolRoster;
|
|
38
|
+
source: string;
|
|
39
|
+
} | {
|
|
40
|
+
status: "blocked";
|
|
41
|
+
reason: string;
|
|
42
|
+
source: string;
|
|
43
|
+
};
|
|
44
|
+
/** Validate and normalize a parsed roster document. Throws on a malformed shape. */
|
|
45
|
+
export declare function parseRoster(raw: unknown): McpToolRoster;
|
|
46
|
+
/** Every name the server can advertise: `static ∪ projectedAdmin`. */
|
|
47
|
+
export declare function advertisableNames(roster: McpToolRoster): Set<string>;
|
|
48
|
+
export declare const VENDORED_ROSTER_DIR: URL;
|
|
49
|
+
/** The honua-server sha the vendored roster provenance is pinned to, if any. */
|
|
50
|
+
export declare function vendoredRosterPin(): string | null;
|
|
51
|
+
/** Load the vendored roster, or report why it is blocked. */
|
|
52
|
+
export declare function loadVendoredRoster(): RosterResolution;
|
|
53
|
+
export interface ResolveRosterOptions {
|
|
54
|
+
env?: NodeJS.ProcessEnv;
|
|
55
|
+
fetchFn?: typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the roster a live certification compares against:
|
|
59
|
+
* 1. `HONUA_MCP_ROSTER_FILE` — an explicit local roster file;
|
|
60
|
+
* 2. the server sha `HONUA_MCP_ROSTER_SERVER_SHA` (the candidate's
|
|
61
|
+
* `ADMIN_LOCAL_SERVER_SHA` in the installer lane), defaulting to the vendored
|
|
62
|
+
* working-contract provenance pin for offline certification:
|
|
63
|
+
* the vendored copy when it is pinned to that sha, otherwise the raw file
|
|
64
|
+
* fetched from honua-server at that sha.
|
|
65
|
+
* Any failure resolves to `blocked` with the reason, never to a pass.
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveRoster(options?: ResolveRosterOptions): Promise<RosterResolution>;
|
|
68
|
+
export interface RosterParityOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Whether the certified topology has a durable control plane (Redis). When
|
|
71
|
+
* false, the `requiresDurableControlPlane` names may legitimately be absent.
|
|
72
|
+
*/
|
|
73
|
+
durableControlPlane: boolean;
|
|
74
|
+
}
|
|
75
|
+
export interface RosterParityResult {
|
|
76
|
+
ok: boolean;
|
|
77
|
+
/** Advertised names the roster does not list in `static ∪ projectedAdmin`. */
|
|
78
|
+
unrostered: string[];
|
|
79
|
+
/** `views.default` names the surface did not advertise. */
|
|
80
|
+
missingDefault: string[];
|
|
81
|
+
/** Retired names the surface still advertises. */
|
|
82
|
+
retiredAdvertised: string[];
|
|
83
|
+
/** `views.default` names absent only because the topology lacks a durable control plane. */
|
|
84
|
+
tolerated: string[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compare a surface's advertised `tools/list` names with the roster:
|
|
88
|
+
* - every advertised name ∈ `static ∪ projectedAdmin`;
|
|
89
|
+
* - every `views.default` name is advertised (the certifier connects without a
|
|
90
|
+
* workflow-view selector, i.e. the default view);
|
|
91
|
+
* - no `retired` name is advertised;
|
|
92
|
+
* - without a durable control plane, `requiresDurableControlPlane` names may be absent.
|
|
93
|
+
*/
|
|
94
|
+
export declare function checkRosterParity(advertised: Iterable<string>, roster: McpToolRoster, options: RosterParityOptions): RosterParityResult;
|
|
95
|
+
/** One-line human summary of a parity result. */
|
|
96
|
+
export declare function describeRosterParity(result: RosterParityResult, advertisedCount: number): string;
|
|
97
|
+
/** Whether the certified topology has a durable control plane (default: yes). */
|
|
98
|
+
export declare function resolveDurableControlPlane(env?: NodeJS.ProcessEnv): boolean;
|
|
99
|
+
//# sourceMappingURL=roster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roster.d.ts","sourceRoot":"","sources":["../../../src/certification/roster.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB,0CAA0C,CAAC;AAExE,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvF,eAAO,MAAM,iBAAiB,EAAE,SAAS,cAAc,EAA4D,CAAC;AAEpH,sEAAsE;AACtE,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,mEAAmE;IACnE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,0EAA0E;IAC1E,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,0FAA0F;IAC1F,2BAA2B,EAAE,MAAM,EAAE,CAAC;IACtC,KAAK,EAAE,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,CAAC,CAAC;IACxC,6CAA6C;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3D;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAS1D,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,aAAa,CAqBvD;AAED,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC,CAEpE;AAsBD,eAAO,MAAM,mBAAmB,KAA6B,CAAC;AAE9D,gFAAgF;AAChF,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAOjD;AAED,6DAA6D;AAC7D,wBAAgB,kBAAkB,IAAI,gBAAgB,CAgBrD;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwDjG;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,mBAAmB,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,2DAA2D;IAC3D,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,4FAA4F;IAC5F,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,EAC5B,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CA2BpB;AAED,iDAAiD;AACjD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,CAehG;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAGxF"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
/**
|
|
4
|
+
* honua-server MCP tool roster (`docs/gis/data/mcp-tool-roster.v1.json`) and the
|
|
5
|
+
* `roster-parity` check the certifier runs against a live `/mcp`.
|
|
6
|
+
*
|
|
7
|
+
* The roster is the server's own generated list of every tool name its composed
|
|
8
|
+
* registry can advertise. It is the authority this repo's catalogs and corpora
|
|
9
|
+
* are checked against, so a retired tool (e.g. `honua_propose_operation`) can no
|
|
10
|
+
* longer linger here after the server drops it.
|
|
11
|
+
*
|
|
12
|
+
* The roster is read BY SERVER SHA: the same honua-server commit the pinned
|
|
13
|
+
* candidate image was built from (`ADMIN_LOCAL_SERVER_SHA` in the installer lane). When the file does
|
|
14
|
+
* not exist at that sha (the server change that adds it has not reached the pin
|
|
15
|
+
* yet), the check reports `blocked` with the reason. It never passes without a
|
|
16
|
+
* roster to compare against.
|
|
17
|
+
*/
|
|
18
|
+
/** Path of the roster inside the honua-server repository. */
|
|
19
|
+
export const ROSTER_REPO_PATH = "docs/gis/data/mcp-tool-roster.v1.json";
|
|
20
|
+
/** Raw-content URL of the roster at a honua-server commit. */
|
|
21
|
+
export function rosterUrl(serverSha) {
|
|
22
|
+
return `https://raw.githubusercontent.com/honua-io/honua-server/${serverSha}/${ROSTER_REPO_PATH}`;
|
|
23
|
+
}
|
|
24
|
+
export const ROSTER_VIEW_NAMES = ["default", "setup", "configure", "operate", "analyze"];
|
|
25
|
+
function readNameArray(value, field) {
|
|
26
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
27
|
+
throw new Error(`roster field "${field}" must be an array of tool-name strings`);
|
|
28
|
+
}
|
|
29
|
+
return [...value];
|
|
30
|
+
}
|
|
31
|
+
/** Validate and normalize a parsed roster document. Throws on a malformed shape. */
|
|
32
|
+
export function parseRoster(raw) {
|
|
33
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
34
|
+
throw new Error("roster must be a JSON object");
|
|
35
|
+
}
|
|
36
|
+
const doc = raw;
|
|
37
|
+
const views = doc.views;
|
|
38
|
+
if (typeof views !== "object" || views === null || Array.isArray(views)) {
|
|
39
|
+
throw new Error('roster field "views" must be an object');
|
|
40
|
+
}
|
|
41
|
+
const viewMap = {};
|
|
42
|
+
for (const view of ROSTER_VIEW_NAMES) {
|
|
43
|
+
viewMap[view] = readNameArray(views[view], `views.${view}`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
serverSha: typeof doc.serverSha === "string" ? doc.serverSha : null,
|
|
47
|
+
static: readNameArray(doc.static, "static"),
|
|
48
|
+
projectedAdmin: readNameArray(doc.projectedAdmin, "projectedAdmin"),
|
|
49
|
+
requiresDurableControlPlane: readNameArray(doc.requiresDurableControlPlane, "requiresDurableControlPlane"),
|
|
50
|
+
views: viewMap,
|
|
51
|
+
retired: readNameArray(doc.retired, "retired"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Every name the server can advertise: `static ∪ projectedAdmin`. */
|
|
55
|
+
export function advertisableNames(roster) {
|
|
56
|
+
return new Set([...roster.static, ...roster.projectedAdmin]);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Vendored copy of the roster, kept beside its provenance so the offline unit
|
|
60
|
+
* test can check this repo's catalog without network access. Probed from both
|
|
61
|
+
* the compiled (`dist/src/certification`) and source (`src/certification`)
|
|
62
|
+
* layouts.
|
|
63
|
+
*/
|
|
64
|
+
function resolveVendoredRosterDir() {
|
|
65
|
+
const candidates = [
|
|
66
|
+
"../../../certification/honua-server-mcp-roster/",
|
|
67
|
+
"../../certification/honua-server-mcp-roster/",
|
|
68
|
+
];
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const url = new URL(candidate, import.meta.url);
|
|
71
|
+
if (existsSync(new URL("PROVENANCE.md", url))) {
|
|
72
|
+
return url;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return new URL(candidates[0], import.meta.url);
|
|
76
|
+
}
|
|
77
|
+
export const VENDORED_ROSTER_DIR = resolveVendoredRosterDir();
|
|
78
|
+
/** The honua-server sha the vendored roster provenance is pinned to, if any. */
|
|
79
|
+
export function vendoredRosterPin() {
|
|
80
|
+
const provenance = new URL("PROVENANCE.md", VENDORED_ROSTER_DIR);
|
|
81
|
+
if (!existsSync(provenance)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const match = /Server commit:\*\* `([0-9a-f]{40})`/.exec(readFileSync(provenance, "utf8"));
|
|
85
|
+
return match ? match[1] : null;
|
|
86
|
+
}
|
|
87
|
+
/** Load the vendored roster, or report why it is blocked. */
|
|
88
|
+
export function loadVendoredRoster() {
|
|
89
|
+
const pin = vendoredRosterPin();
|
|
90
|
+
const file = new URL("mcp-tool-roster.v1.json", VENDORED_ROSTER_DIR);
|
|
91
|
+
const source = `vendored ${fileURLToPath(file)}${pin ? ` (honua-server@${pin})` : ""}`;
|
|
92
|
+
if (!existsSync(file)) {
|
|
93
|
+
return {
|
|
94
|
+
status: "blocked",
|
|
95
|
+
source,
|
|
96
|
+
reason: `no vendored roster: ${ROSTER_REPO_PATH} does not exist at the pinned honua-server@${pin ?? "(unpinned)"} yet`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
return { status: "loaded", roster: parseRoster(JSON.parse(readFileSync(file, "utf8"))), source };
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return { status: "blocked", source, reason: `vendored roster is malformed: ${errorMessage(err)}` };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the roster a live certification compares against:
|
|
108
|
+
* 1. `HONUA_MCP_ROSTER_FILE` — an explicit local roster file;
|
|
109
|
+
* 2. the server sha `HONUA_MCP_ROSTER_SERVER_SHA` (the candidate's
|
|
110
|
+
* `ADMIN_LOCAL_SERVER_SHA` in the installer lane), defaulting to the vendored
|
|
111
|
+
* working-contract provenance pin for offline certification:
|
|
112
|
+
* the vendored copy when it is pinned to that sha, otherwise the raw file
|
|
113
|
+
* fetched from honua-server at that sha.
|
|
114
|
+
* Any failure resolves to `blocked` with the reason, never to a pass.
|
|
115
|
+
*/
|
|
116
|
+
export async function resolveRoster(options = {}) {
|
|
117
|
+
const env = options.env ?? process.env;
|
|
118
|
+
const file = env.HONUA_MCP_ROSTER_FILE?.trim();
|
|
119
|
+
if (file) {
|
|
120
|
+
try {
|
|
121
|
+
return { status: "loaded", roster: parseRoster(JSON.parse(readFileSync(file, "utf8"))), source: file };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
return { status: "blocked", source: file, reason: `roster file unreadable: ${errorMessage(err)}` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const sha = env.HONUA_MCP_ROSTER_SERVER_SHA?.trim() || vendoredRosterPin();
|
|
128
|
+
if (!sha) {
|
|
129
|
+
return {
|
|
130
|
+
status: "blocked",
|
|
131
|
+
source: "(none)",
|
|
132
|
+
reason: "no honua-server sha to read the roster at (set HONUA_MCP_ROSTER_SERVER_SHA)",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (!/^[0-9a-f]{7,40}$/.test(sha)) {
|
|
136
|
+
return { status: "blocked", source: sha, reason: `HONUA_MCP_ROSTER_SERVER_SHA is not a commit sha: ${sha}` };
|
|
137
|
+
}
|
|
138
|
+
if (vendoredRosterPin() === sha) {
|
|
139
|
+
const vendored = loadVendoredRoster();
|
|
140
|
+
if (vendored.status === "loaded") {
|
|
141
|
+
return vendored;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const url = rosterUrl(sha);
|
|
145
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
146
|
+
// honua-server may be private to this workflow's token; a read token makes a
|
|
147
|
+
// 404 mean "absent at that sha" rather than "not visible".
|
|
148
|
+
const token = env.HONUA_MCP_ROSTER_TOKEN?.trim();
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetchFn(url, {
|
|
151
|
+
redirect: "follow",
|
|
152
|
+
...(token ? { headers: { Authorization: `token ${token}` } } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (response.status === 404) {
|
|
155
|
+
return {
|
|
156
|
+
status: "blocked",
|
|
157
|
+
source: url,
|
|
158
|
+
reason: token
|
|
159
|
+
? `${ROSTER_REPO_PATH} does not exist at honua-server@${sha} (the roster has not reached the pinned server sha yet)`
|
|
160
|
+
: `${ROSTER_REPO_PATH} not found at honua-server@${sha}: absent at that sha, or not readable without HONUA_MCP_ROSTER_TOKEN`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
return { status: "blocked", source: url, reason: `roster fetch failed with HTTP ${response.status}` };
|
|
165
|
+
}
|
|
166
|
+
return { status: "loaded", roster: parseRoster(await response.json()), source: url };
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
return { status: "blocked", source: url, reason: `roster fetch failed: ${errorMessage(err)}` };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Compare a surface's advertised `tools/list` names with the roster:
|
|
174
|
+
* - every advertised name ∈ `static ∪ projectedAdmin`;
|
|
175
|
+
* - every `views.default` name is advertised (the certifier connects without a
|
|
176
|
+
* workflow-view selector, i.e. the default view);
|
|
177
|
+
* - no `retired` name is advertised;
|
|
178
|
+
* - without a durable control plane, `requiresDurableControlPlane` names may be absent.
|
|
179
|
+
*/
|
|
180
|
+
export function checkRosterParity(advertised, roster, options) {
|
|
181
|
+
const advertisedSet = new Set(advertised);
|
|
182
|
+
const known = advertisableNames(roster);
|
|
183
|
+
const durableOnly = new Set(roster.requiresDurableControlPlane);
|
|
184
|
+
const retired = new Set(roster.retired);
|
|
185
|
+
const unrostered = [...advertisedSet].filter((name) => !known.has(name) && !retired.has(name)).sort();
|
|
186
|
+
const retiredAdvertised = [...advertisedSet].filter((name) => retired.has(name)).sort();
|
|
187
|
+
const missingDefault = [];
|
|
188
|
+
const tolerated = [];
|
|
189
|
+
for (const name of roster.views.default) {
|
|
190
|
+
if (advertisedSet.has(name))
|
|
191
|
+
continue;
|
|
192
|
+
if (!options.durableControlPlane && durableOnly.has(name)) {
|
|
193
|
+
tolerated.push(name);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
missingDefault.push(name);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
missingDefault.sort();
|
|
200
|
+
tolerated.sort();
|
|
201
|
+
return {
|
|
202
|
+
ok: unrostered.length === 0 && missingDefault.length === 0 && retiredAdvertised.length === 0,
|
|
203
|
+
unrostered,
|
|
204
|
+
missingDefault,
|
|
205
|
+
retiredAdvertised,
|
|
206
|
+
tolerated,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** One-line human summary of a parity result. */
|
|
210
|
+
export function describeRosterParity(result, advertisedCount) {
|
|
211
|
+
if (result.ok) {
|
|
212
|
+
const tolerated = result.tolerated.length > 0
|
|
213
|
+
? `; ${result.tolerated.length} durable-control-plane tool(s) absent on this topology: ${result.tolerated.join(", ")}`
|
|
214
|
+
: "";
|
|
215
|
+
return `all ${advertisedCount} advertised tools are rostered, views.default is fully advertised, no retired tool is advertised${tolerated}`;
|
|
216
|
+
}
|
|
217
|
+
const parts = [];
|
|
218
|
+
if (result.retiredAdvertised.length > 0)
|
|
219
|
+
parts.push(`retired tool(s) advertised: ${result.retiredAdvertised.join(", ")}`);
|
|
220
|
+
if (result.unrostered.length > 0)
|
|
221
|
+
parts.push(`advertised but not in roster: ${result.unrostered.join(", ")}`);
|
|
222
|
+
if (result.missingDefault.length > 0)
|
|
223
|
+
parts.push(`views.default tool(s) not advertised: ${result.missingDefault.join(", ")}`);
|
|
224
|
+
return parts.join("; ");
|
|
225
|
+
}
|
|
226
|
+
/** Whether the certified topology has a durable control plane (default: yes). */
|
|
227
|
+
export function resolveDurableControlPlane(env = process.env) {
|
|
228
|
+
const raw = (env.HONUA_MCP_CERT_TOPOLOGY ?? "durable").trim().toLowerCase();
|
|
229
|
+
return raw !== "redis-off" && raw !== "ephemeral";
|
|
230
|
+
}
|
|
231
|
+
function errorMessage(err) {
|
|
232
|
+
return err instanceof Error ? err.message : String(err);
|
|
233
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { RoundTripEnv } from "./fixtures.js";
|
|
3
|
+
/**
|
|
4
|
+
* Round-trip target discovery for a freshly booted candidate.
|
|
5
|
+
*
|
|
6
|
+
* The read-only round trips need a published layer. The offline mock seeds
|
|
7
|
+
* `svc-parks`, but a fresh `honua admin install local` candidate publishes
|
|
8
|
+
* nothing under that id. Setting `HONUA_MCP_SERVICE_ID=discover` makes the
|
|
9
|
+
* certifier ask the candidate itself (`honua_list_layers`) and bind the round
|
|
10
|
+
* trips to the first published layer. When the candidate serves no layer the
|
|
11
|
+
* `round-trip-target` contract is `blocked`: the evidence could not be gathered,
|
|
12
|
+
* so the summary cannot pass, and the service-bound tools are not called with a
|
|
13
|
+
* made-up id.
|
|
14
|
+
*/
|
|
15
|
+
/** Sentinel value of `HONUA_MCP_SERVICE_ID` that asks for discovery. */
|
|
16
|
+
export declare const DISCOVER_SERVICE_ID = "discover";
|
|
17
|
+
/** The tool the target is discovered from. */
|
|
18
|
+
export declare const LIST_LAYERS_TOOL = "honua_list_layers";
|
|
19
|
+
/** Round-trip fixtures that address a published service/layer. */
|
|
20
|
+
export declare const SERVICE_BOUND_TOOLS: readonly string[];
|
|
21
|
+
export type RoundTripTargetResolution = {
|
|
22
|
+
status: "passed";
|
|
23
|
+
serviceId: string;
|
|
24
|
+
layerId: number;
|
|
25
|
+
detail: string;
|
|
26
|
+
} | {
|
|
27
|
+
status: "blocked";
|
|
28
|
+
detail: string;
|
|
29
|
+
};
|
|
30
|
+
/** Whether the resolved round-trip env asks for discovery. */
|
|
31
|
+
export declare function wantsDiscovery(env: RoundTripEnv): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Pick the first published layer from a `honua_list_layers` payload
|
|
34
|
+
* (`structuredContent`, or the JSON text content when no structured output is
|
|
35
|
+
* returned). Accepts camelCase and snake_case identifiers.
|
|
36
|
+
*/
|
|
37
|
+
export declare function firstPublishedLayer(payload: unknown): {
|
|
38
|
+
serviceId: string;
|
|
39
|
+
layerId: number;
|
|
40
|
+
} | undefined;
|
|
41
|
+
/** Ask the candidate for its first published layer. Never throws. */
|
|
42
|
+
export declare function discoverRoundTripTarget(client: Pick<Client, "callTool">, advertisedNames: Set<string>): Promise<RoundTripTargetResolution>;
|
|
43
|
+
/** Drop the round-trip fixtures that would address an undiscovered service. */
|
|
44
|
+
export declare function withoutServiceBoundInputs(inputs: Record<string, Record<string, unknown>>): Record<string, Record<string, unknown>>;
|
|
45
|
+
//# sourceMappingURL=round-trip-target.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"round-trip-target.d.ts","sourceRoot":"","sources":["../../../src/certification/round-trip-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD;;;;;;;;;;;GAWG;AAEH,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAE9C,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD,kEAAkE;AAClE,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAOhD,CAAC;AAEF,MAAM,MAAM,yBAAyB,GACjC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,8DAA8D;AAC9D,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAEzD;AAWD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAoBxG;AAsBD,qEAAqE;AACrE,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,EAChC,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAC3B,OAAO,CAAC,yBAAyB,CAAC,CAkCpC;AAED,+EAA+E;AAC/E,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC9C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMzC"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round-trip target discovery for a freshly booted candidate.
|
|
3
|
+
*
|
|
4
|
+
* The read-only round trips need a published layer. The offline mock seeds
|
|
5
|
+
* `svc-parks`, but a fresh `honua admin install local` candidate publishes
|
|
6
|
+
* nothing under that id. Setting `HONUA_MCP_SERVICE_ID=discover` makes the
|
|
7
|
+
* certifier ask the candidate itself (`honua_list_layers`) and bind the round
|
|
8
|
+
* trips to the first published layer. When the candidate serves no layer the
|
|
9
|
+
* `round-trip-target` contract is `blocked`: the evidence could not be gathered,
|
|
10
|
+
* so the summary cannot pass, and the service-bound tools are not called with a
|
|
11
|
+
* made-up id.
|
|
12
|
+
*/
|
|
13
|
+
/** Sentinel value of `HONUA_MCP_SERVICE_ID` that asks for discovery. */
|
|
14
|
+
export const DISCOVER_SERVICE_ID = "discover";
|
|
15
|
+
/** The tool the target is discovered from. */
|
|
16
|
+
export const LIST_LAYERS_TOOL = "honua_list_layers";
|
|
17
|
+
/** Round-trip fixtures that address a published service/layer. */
|
|
18
|
+
export const SERVICE_BOUND_TOOLS = [
|
|
19
|
+
"honua_describe_layer",
|
|
20
|
+
"honua_count_features",
|
|
21
|
+
"honua_get_extent",
|
|
22
|
+
"honua_statistics",
|
|
23
|
+
"honua_query_features",
|
|
24
|
+
"honua_render_map",
|
|
25
|
+
];
|
|
26
|
+
/** Whether the resolved round-trip env asks for discovery. */
|
|
27
|
+
export function wantsDiscovery(env) {
|
|
28
|
+
return env.serviceId === DISCOVER_SERVICE_ID;
|
|
29
|
+
}
|
|
30
|
+
function asLayerId(value) {
|
|
31
|
+
const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
|
|
32
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 0 ? n : undefined;
|
|
33
|
+
}
|
|
34
|
+
function asServiceId(value) {
|
|
35
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Pick the first published layer from a `honua_list_layers` payload
|
|
39
|
+
* (`structuredContent`, or the JSON text content when no structured output is
|
|
40
|
+
* returned). Accepts camelCase and snake_case identifiers.
|
|
41
|
+
*/
|
|
42
|
+
export function firstPublishedLayer(payload) {
|
|
43
|
+
if (!payload || typeof payload !== "object") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const layers = payload.layers;
|
|
47
|
+
if (!Array.isArray(layers)) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
for (const layer of layers) {
|
|
51
|
+
if (!layer || typeof layer !== "object") {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const l = layer;
|
|
55
|
+
const serviceId = asServiceId(l.serviceId ?? l.service_id);
|
|
56
|
+
const layerId = asLayerId(l.layerId ?? l.layer_id);
|
|
57
|
+
if (serviceId && layerId !== undefined) {
|
|
58
|
+
return { serviceId, layerId };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
function payloadOf(result) {
|
|
64
|
+
if (result.structuredContent !== undefined) {
|
|
65
|
+
return result.structuredContent;
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(result.content)) {
|
|
68
|
+
for (const part of result.content) {
|
|
69
|
+
const text = part.type === "text" ? part.text : undefined;
|
|
70
|
+
if (typeof text === "string") {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(text);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// not JSON; keep looking
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
/** Ask the candidate for its first published layer. Never throws. */
|
|
83
|
+
export async function discoverRoundTripTarget(client, advertisedNames) {
|
|
84
|
+
if (!advertisedNames.has(LIST_LAYERS_TOOL)) {
|
|
85
|
+
return {
|
|
86
|
+
status: "blocked",
|
|
87
|
+
detail: `${LIST_LAYERS_TOOL} is not advertised, so no published layer can be discovered`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const result = (await client.callTool({ name: LIST_LAYERS_TOOL, arguments: {} }));
|
|
92
|
+
if (result.isError) {
|
|
93
|
+
return { status: "blocked", detail: `${LIST_LAYERS_TOOL} returned isError=true; no published layer discovered` };
|
|
94
|
+
}
|
|
95
|
+
const layer = firstPublishedLayer(payloadOf(result));
|
|
96
|
+
if (!layer) {
|
|
97
|
+
return {
|
|
98
|
+
status: "blocked",
|
|
99
|
+
detail: `the candidate serves no published layer (${LIST_LAYERS_TOOL} returned none); publish a service before certifying the round trips`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
status: "passed",
|
|
104
|
+
...layer,
|
|
105
|
+
detail: `round trips bound to ${layer.serviceId} layer ${layer.layerId} (first layer from ${LIST_LAYERS_TOOL})`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
return {
|
|
110
|
+
status: "blocked",
|
|
111
|
+
detail: `${LIST_LAYERS_TOOL} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Drop the round-trip fixtures that would address an undiscovered service. */
|
|
116
|
+
export function withoutServiceBoundInputs(inputs) {
|
|
117
|
+
const kept = { ...inputs };
|
|
118
|
+
for (const name of SERVICE_BOUND_TOOLS) {
|
|
119
|
+
delete kept[name];
|
|
120
|
+
}
|
|
121
|
+
return kept;
|
|
122
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/certification/run.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/certification/run.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,gBAAgB,CAAC;AAGnE,OAAO,EAAE,KAAK,mBAAmB,EAA2B,MAAM,aAAa,CAAC;AAEhF;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAED,wBAAsB,gBAAgB,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAI7F;AAED,8DAA8D;AAC9D,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAsB9B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,GAAG,IAAI,CAGtF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { certify } from "./certifier.js";
|
|
3
3
|
import { renderMarkdown } from "./report.js";
|
|
4
|
+
import { resolveDurableControlPlane, resolveRoster } from "./roster.js";
|
|
4
5
|
import { openCertificationTarget } from "./target.js";
|
|
5
6
|
export async function runCertification(options = {}) {
|
|
6
7
|
const env = options.env ?? process.env;
|
|
@@ -10,6 +11,11 @@ export async function runCertification(options = {}) {
|
|
|
10
11
|
/** Certify an already-opened target, closing it when done. */
|
|
11
12
|
export async function certifyTarget(target, env = process.env) {
|
|
12
13
|
try {
|
|
14
|
+
// Roster parity is a product-surface contract: only a live honua /mcp
|
|
15
|
+
// (directly or through the stdio proxy) is held to the server roster.
|
|
16
|
+
const roster = target.backend === "live" && (target.mode === "remote" || target.mode === "stdio-proxy")
|
|
17
|
+
? await resolveRoster({ env })
|
|
18
|
+
: undefined;
|
|
13
19
|
return await certify({
|
|
14
20
|
client: target.client,
|
|
15
21
|
targetMode: target.mode,
|
|
@@ -19,6 +25,7 @@ export async function certifyTarget(target, env = process.env) {
|
|
|
19
25
|
authMode: target.authMode,
|
|
20
26
|
connectUnauthenticated: target.supportsUnauthenticatedPass ? () => target.connectUnauthenticated() : undefined,
|
|
21
27
|
env,
|
|
28
|
+
...(roster ? { roster, durableControlPlane: resolveDurableControlPlane(env) } : {}),
|
|
22
29
|
});
|
|
23
30
|
}
|
|
24
31
|
finally {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"northstar-corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/northstar-corpus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"northstar-corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/northstar-corpus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAkF3C,eAAO,MAAM,gBAAgB,EAAE,QAAQ,EAyHtC,CAAC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TYPED_PROPOSAL_TOOLS } from "./proposal-tools.js";
|
|
1
2
|
/**
|
|
2
3
|
* NORTH-STAR corpus — the P1 gate measure (honua-server #1948, MCP initiative P1.7).
|
|
3
4
|
*
|
|
@@ -37,12 +38,13 @@
|
|
|
37
38
|
* with `--driver bedrock` + `HONUA_MCP_REMOTE_URL=<server>/mcp` + credentials.
|
|
38
39
|
*/
|
|
39
40
|
/**
|
|
40
|
-
* The
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* The governed control-plane proposal roster (the typed tools that replaced the
|
|
42
|
+
* retired `honua_propose_operation`). Every north-star workflow must compose the
|
|
43
|
+
* structured lifecycle (plan → validate → execute → publish) rather than file a
|
|
44
|
+
* control-plane proposal, so forbidding the roster turns "did the client use the
|
|
43
45
|
* intended workflow?" into a graded criterion.
|
|
44
46
|
*/
|
|
45
|
-
const
|
|
47
|
+
const PROPOSAL_TOOLS = [...TYPED_PROPOSAL_TOOLS];
|
|
46
48
|
/** A small, embedded address CSV for the geocode → publish workflow (10 rows). */
|
|
47
49
|
const ADDRESS_CSV = [
|
|
48
50
|
"name,street,city,state,zip",
|
|
@@ -71,7 +73,7 @@ const CSV_GEOCODE_PROMPT = `Here is a CSV of addresses. Geocode them and publish
|
|
|
71
73
|
${ADDRESS_CSV}
|
|
72
74
|
\`\`\`
|
|
73
75
|
|
|
74
|
-
Batch-geocode every row, ingest the geocoded points as a dataset, and publish that dataset as a hosted point service. Do not
|
|
76
|
+
Batch-geocode every row, ingest the geocoded points as a dataset, and publish that dataset as a hosted point service. Do not file a control-plane proposal (honua_propose_*).`;
|
|
75
77
|
export const NORTHSTAR_CORPUS = [
|
|
76
78
|
{
|
|
77
79
|
id: "northstar-styled-map",
|
|
@@ -80,13 +82,13 @@ export const NORTHSTAR_CORPUS = [
|
|
|
80
82
|
prompt: "Find all parcels within 500 m of a flood zone and produce a styled map of them. " +
|
|
81
83
|
"Resolve the parcels and flood-zone layers, plan and run the proximity analysis as a " +
|
|
82
84
|
"governed job, publish the result as a new layer, style it, and render a map. Use the " +
|
|
83
|
-
"structured analysis lifecycle — do not
|
|
85
|
+
"structured analysis lifecycle — do not file a control-plane proposal (honua_propose_*).",
|
|
84
86
|
criteria: {
|
|
85
87
|
// Load-bearing arc of the workflow: ground the entities, execute the plan,
|
|
86
88
|
// publish the job artifact to a layer, and render a map of it.
|
|
87
89
|
requiredTools: ["honua_resolve_entity", "honua_execute_plan", "honua_publish_result", "honua_render_map"],
|
|
88
90
|
expectedToolSequence: ["honua_resolve_entity", "honua_execute_plan", "honua_publish_result", "honua_render_map"],
|
|
89
|
-
forbiddenTools:
|
|
91
|
+
forbiddenTools: PROPOSAL_TOOLS,
|
|
90
92
|
},
|
|
91
93
|
script: [
|
|
92
94
|
{ tool: "honua_resolve_entity", args: { text: "parcels", entityType: "layer" } },
|
|
@@ -121,7 +123,7 @@ export const NORTHSTAR_CORPUS = [
|
|
|
121
123
|
// ingest the resulting points → publish the source table as a service.
|
|
122
124
|
requiredTools: ["honua_geocode_addresses", "honua_ingest_dataset", "honua_publish_service"],
|
|
123
125
|
expectedToolSequence: ["honua_geocode_addresses", "honua_ingest_dataset", "honua_publish_service"],
|
|
124
|
-
forbiddenTools:
|
|
126
|
+
forbiddenTools: PROPOSAL_TOOLS,
|
|
125
127
|
},
|
|
126
128
|
script: [
|
|
127
129
|
{ tool: "honua_geocode_addresses", args: { addresses: ADDRESS_ROWS } },
|
|
@@ -153,7 +155,7 @@ export const NORTHSTAR_CORPUS = [
|
|
|
153
155
|
prompt: "Buffer highways by 100 m, intersect the buffers with wetlands, and give me summary " +
|
|
154
156
|
"statistics for the overlap. Run it as a job and report progress. Ground the goal into " +
|
|
155
157
|
"an analysis plan, validate it, then execute it as a governed job and report the results " +
|
|
156
|
-
"the job produces. Do not
|
|
158
|
+
"the job produces. Do not file a control-plane proposal (honua_propose_*).",
|
|
157
159
|
criteria: {
|
|
158
160
|
// Plan entry → validate → execute-as-job. The answer must carry evidence
|
|
159
161
|
// that the JOB path was used and its results read back — kept loose (the
|
|
@@ -166,7 +168,7 @@ export const NORTHSTAR_CORPUS = [
|
|
|
166
168
|
"honua_validate_plan",
|
|
167
169
|
"honua_execute_plan",
|
|
168
170
|
],
|
|
169
|
-
forbiddenTools:
|
|
171
|
+
forbiddenTools: PROPOSAL_TOOLS,
|
|
170
172
|
answerMustInclude: ["job"],
|
|
171
173
|
},
|
|
172
174
|
script: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operator-corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/operator-corpus.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"operator-corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/operator-corpus.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AA+D3C,eAAO,MAAM,eAAe,EAAE,QAAQ,EAmJrC,CAAC"}
|