@aiwg/cli 2026.9.1 → 2026.9.3
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/agentic/code/providers/antigravity/provider-contract.v1.json +121 -0
- package/agentic/code/providers/capability-matrix.yaml +88 -2
- package/agentic/code/providers/model-capabilities.v1.json +42 -0
- package/agentic/code/providers/model-catalog.v1.json +37 -0
- package/agentic/code/providers/omp/README.md +58 -0
- package/agentic/code/providers/omp/aiwg-bridge.ts +52 -0
- package/agentic/code/providers/pi/aiwg-bridge.ts +26 -0
- package/dist/src/agents/agent-deployer.js +18 -0
- package/dist/src/agents/agent-packager.js +25 -0
- package/dist/src/artifacts/backends/sqlite-backend.js +18 -10
- package/dist/src/artifacts/query-engine.js +30 -0
- package/dist/src/auth/credential-store.js +6 -0
- package/dist/src/cli/agent-spawn.js +13 -2
- package/dist/src/cli/handlers/help.js +3 -1
- package/dist/src/cli/handlers/init.js +2 -0
- package/dist/src/cli/handlers/models.js +1 -1
- package/dist/src/cli/handlers/runtime-info.js +8 -1
- package/dist/src/cli/handlers/session.js +5 -4
- package/dist/src/cli/handlers/sessions.js +54 -19
- package/dist/src/cli/handlers/setup.js +9 -2
- package/dist/src/cli/handlers/steward.js +13 -2
- package/dist/src/cli/handlers/subcommands.js +11 -0
- package/dist/src/cli/handlers/team.js +68 -7
- package/dist/src/cli/handlers/use.js +121 -9
- package/dist/src/cli/scope-resolver.js +7 -0
- package/dist/src/config/aiwg-config.js +1 -0
- package/dist/src/dataset/fortemi-live-qualification.d.ts +53 -0
- package/dist/src/dataset/fortemi-live-qualification.js +297 -0
- package/dist/src/dataset/index.d.ts +1 -0
- package/dist/src/dataset/index.js +1 -0
- package/dist/src/mcp/cli.mjs +30 -1
- package/dist/src/mcp/omp-config.mjs +128 -0
- package/dist/src/mcp/registry.js +45 -5
- package/dist/src/mcp/registry.mjs +29 -6
- package/dist/src/models/model-capabilities.v1.json +42 -0
- package/dist/src/models/model-catalog.v1.json +37 -0
- package/dist/src/models/model-discovery.js +78 -5
- package/dist/src/models/provider-policy.js +6 -3
- package/dist/src/plugin/skill-command-translator.js +2 -0
- package/dist/src/providers/capability-matrix.yaml +88 -2
- package/dist/src/providers/omp-agent.mjs +40 -0
- package/dist/src/providers/omp-diagnostics.mjs +15 -0
- package/dist/src/providers/omp-paths.mjs +38 -0
- package/dist/src/providers/provider-definitions.js +83 -0
- package/dist/src/providers/provider-definitions.mjs +25 -1
- package/dist/src/providers/provider-inventory.js +2 -0
- package/dist/src/sessions/adapters/omp.js +203 -0
- package/dist/src/sessions/adapters/pi.js +141 -0
- package/dist/src/sessions/batch-import.js +7 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/importer.js +4 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/readers.js +4 -3
- package/dist/src/sessions/workspace-discovery.js +22 -2
- package/dist/src/skills/deployer.js +21 -1
- package/dist/src/smiths/agentsmith/generator.js +1 -0
- package/dist/src/smiths/context-pipeline/parallelism-section.js +2 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +2 -2
- package/dist/src/smiths/context-pipeline/workspace-context.js +2 -2
- package/dist/src/storage/backends/fortemi.js +142 -17
- package/dist/src/storage/fortemi-qualification-receipt.js +206 -0
- package/dist/src/storage/fortemi-qualification.js +67 -6
- package/dist/src/storage/index.js +1 -0
- package/package.json +2 -1
- package/schemas/dataset/fortemi-live-qualification-receipt.v1.schema.json +132 -0
- package/tools/agents/deploy-agents.mjs +6 -3
- package/tools/agents/providers/antigravity.mjs +147 -0
- package/tools/agents/providers/omp.d.mts +4 -0
- package/tools/agents/providers/omp.mjs +256 -0
- package/tools/agents/providers/pi.mjs +13 -1
- package/tools/providers/antigravity-transport.mjs +124 -0
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import fs from 'fs/promises';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import os from 'os';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { resolveOmpPaths } from '../../providers/omp-paths.mjs';
|
|
15
17
|
import YAML from 'yaml';
|
|
16
18
|
import { createScriptRunner } from './script-runner.js';
|
|
17
19
|
import { getFrameworkRoot, getVersionInfo } from '../../channel/manager.mjs';
|
|
@@ -1041,10 +1043,10 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1041
1043
|
}
|
|
1042
1044
|
const paths = getProviderPaths(provider);
|
|
1043
1045
|
const kernelSkillsPath = getProviderKernelSkillsPath(provider);
|
|
1044
|
-
const deployRoots =
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1046
|
+
const deployRoots = provider === 'omp' && options.scope === 'user'
|
|
1047
|
+
? [path.join(resolveOmpPaths({ cwd: target }).agentDir, 'skills')]
|
|
1048
|
+
: [...new Set([paths.skills, kernelSkillsPath]
|
|
1049
|
+
.filter((value) => Boolean(value)).map(value => resolveDeployPath(target, value)))];
|
|
1048
1050
|
for (const skillName of skillDirs) {
|
|
1049
1051
|
const sourceSkillDir = path.join(skillsRoot, skillName);
|
|
1050
1052
|
const sourceSkillMd = path.join(sourceSkillDir, 'SKILL.md');
|
|
@@ -1055,6 +1057,8 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1055
1057
|
catch {
|
|
1056
1058
|
continue;
|
|
1057
1059
|
}
|
|
1060
|
+
if (options.skipUndeployed && !(await Promise.all(deployRoots.map(root => fileExists(path.join(root, skillName, 'SKILL.md'))))).some(Boolean))
|
|
1061
|
+
continue;
|
|
1058
1062
|
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
|
|
1059
1063
|
const declaredEntrypoint = frontmatter
|
|
1060
1064
|
.match(/^[ \t]+entrypoint:\s*["']?([^"'\s]+)["']?\s*$/m)?.[1];
|
|
@@ -1097,6 +1101,11 @@ async function reconcileDeployedSkillAssets(bundlePath, target, provider, option
|
|
|
1097
1101
|
if (!deployedSkillRoot)
|
|
1098
1102
|
throw new Error(`deployed skill '${skillName}' not found while reconciling support assets`);
|
|
1099
1103
|
const destination = path.join(deployedSkillRoot, skillName, ...normalized.split('/'));
|
|
1104
|
+
if (provider === 'omp') {
|
|
1105
|
+
const adapter = await import(pathToFileURL(path.join(await getFrameworkRoot(), 'tools/agents/providers/omp.mjs')).href);
|
|
1106
|
+
adapter.deploySkillSupportAsset(source, destination, { quiet: true });
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1100
1109
|
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
1101
1110
|
await fs.copyFile(source, destination);
|
|
1102
1111
|
const mode = (await fs.stat(source)).mode & 0o777;
|
|
@@ -1348,6 +1357,18 @@ async function deployProjectLocalBundles(opts) {
|
|
|
1348
1357
|
}
|
|
1349
1358
|
}
|
|
1350
1359
|
}
|
|
1360
|
+
// Automatic reconciliation (also used by refresh/upgrade) must restore the
|
|
1361
|
+
// project search cache for existing bundles, just as a named local install
|
|
1362
|
+
// does. Named installs refresh once after all selected providers finish.
|
|
1363
|
+
if (deployed > 0 && !dryRun && !onlyBundleId) {
|
|
1364
|
+
try {
|
|
1365
|
+
await rebuildExternalBundleIndex(projectDir, 'project', verbose);
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
failed++;
|
|
1369
|
+
ui.warn(`Project-local index refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1351
1372
|
// Refresh the project kernel quickref from either legacy operator input or
|
|
1352
1373
|
// managed project-local discovery whenever bundles deploy.
|
|
1353
1374
|
const { hasProjectQuickref, deployProjectQuickref } = await import('../../extensions/project-quickref.js');
|
|
@@ -1968,6 +1989,65 @@ async function rebuildExternalBundleIndex(projectDir, graph, verbose) {
|
|
|
1968
1989
|
}
|
|
1969
1990
|
ui.dim(` Refreshed ${graph}-scope capability index`);
|
|
1970
1991
|
}
|
|
1992
|
+
/** User OMP writes use native ownership receipts instead of an unconditional copy. */
|
|
1993
|
+
async function deployOmpUserSource(opts) {
|
|
1994
|
+
const adapter = await import(pathToFileURL(path.join(opts.frameworkRoot, 'tools/agents/providers/omp.mjs')).href);
|
|
1995
|
+
await adapter.deploy({ srcRoot: opts.source, target: opts.target, provider: 'omp', scope: 'user',
|
|
1996
|
+
mode: opts.mode ?? 'general', deployCommands: true, deploySkills: true, deployRules: true,
|
|
1997
|
+
copyStandardSkills: opts.copyAll, quiet: true, deployVersion: (await getVersionInfo()).version });
|
|
1998
|
+
const sourceBundles = new Set([opts.source]);
|
|
1999
|
+
const { resourceDirs } = resolveOmpPaths({ cwd: opts.target });
|
|
2000
|
+
const entries = {
|
|
2001
|
+
agents: [], commands: [], skills: [], rules: [], behaviors: [],
|
|
2002
|
+
};
|
|
2003
|
+
const belongsToBundle = (entry) => {
|
|
2004
|
+
if (entry.provider !== 'omp' || typeof entry.source !== 'string')
|
|
2005
|
+
return false;
|
|
2006
|
+
if (entry.transformation === 'omp-extension')
|
|
2007
|
+
return true;
|
|
2008
|
+
const relative = path.relative(opts.source, entry.source);
|
|
2009
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
2010
|
+
};
|
|
2011
|
+
const locations = resourceDirs;
|
|
2012
|
+
for (const kind of Object.keys(entries)) {
|
|
2013
|
+
const directory = locations[kind];
|
|
2014
|
+
if (kind === 'skills') {
|
|
2015
|
+
let children;
|
|
2016
|
+
try {
|
|
2017
|
+
children = await fs.readdir(directory);
|
|
2018
|
+
}
|
|
2019
|
+
catch {
|
|
2020
|
+
continue;
|
|
2021
|
+
}
|
|
2022
|
+
for (const child of children) {
|
|
2023
|
+
try {
|
|
2024
|
+
const receipt = JSON.parse(await fs.readFile(path.join(directory, child, '.aiwg-manifest.json'), 'utf8'));
|
|
2025
|
+
if (receipt.managed?.['SKILL.md'] && belongsToBundle(receipt.managed['SKILL.md'])) {
|
|
2026
|
+
entries.skills.push(child);
|
|
2027
|
+
const source = receipt.managed['SKILL.md'].source;
|
|
2028
|
+
if (typeof source === 'string')
|
|
2029
|
+
sourceBundles.add(path.dirname(path.dirname(path.dirname(source))));
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
catch { /* operator resources have no OMP receipt */ }
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
try {
|
|
2037
|
+
const receipt = JSON.parse(await fs.readFile(path.join(directory, '.aiwg-manifest.json'), 'utf8'));
|
|
2038
|
+
entries[kind] = Object.entries(receipt.managed ?? {}).filter(([, entry]) => belongsToBundle(entry)).map(([name]) => name);
|
|
2039
|
+
}
|
|
2040
|
+
catch { /* no deployed resources of this kind */ }
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
for (const source of sourceBundles)
|
|
2044
|
+
await reconcileDeployedSkillAssets(source, opts.target, 'omp', { strictReferences: false, scope: 'user', skipUndeployed: !opts.copyAll });
|
|
2045
|
+
const { recordUserDeploy } = await import('../../config/user-registry.js');
|
|
2046
|
+
await recordUserDeploy({ framework: opts.bundle, provider: 'omp',
|
|
2047
|
+
version: (await getVersionInfo()).version, source: 'bundled',
|
|
2048
|
+
counts: { agents: entries.agents.length, commands: entries.commands.length,
|
|
2049
|
+
skills: entries.skills.length, rules: entries.rules.length }, entries });
|
|
2050
|
+
}
|
|
1971
2051
|
async function mirrorProjectLocalBundleToUserScope(opts) {
|
|
1972
2052
|
const paths = getProviderPaths(opts.provider);
|
|
1973
2053
|
const resolveProjectPath = (value) => !value ? '' : path.isAbsolute(value) ? value : path.join(opts.target, value);
|
|
@@ -2701,10 +2781,25 @@ export class UseHandler {
|
|
|
2701
2781
|
if (isAddon || isExtension) {
|
|
2702
2782
|
const providerIdx = remainingArgs.findIndex(a => a === '--provider' || a === '--platform');
|
|
2703
2783
|
const explicitAddonProvider = providerIdx >= 0 && remainingArgs[providerIdx + 1] ? remainingArgs[providerIdx + 1] : null;
|
|
2704
|
-
const provider = explicitAddonProvider ?? (config?.providers?.[0] ?? 'claude');
|
|
2784
|
+
const provider = resolveBuiltInProviderForUse(explicitAddonProvider ?? (config?.providers?.[0] ?? 'claude')).provider;
|
|
2705
2785
|
const targetIdx = remainingArgs.findIndex(a => a === '--target');
|
|
2706
2786
|
const target = targetIdx >= 0 && remainingArgs[targetIdx + 1] ? remainingArgs[targetIdx + 1] : process.cwd();
|
|
2707
2787
|
const dryRunAddon = remainingArgs.includes('--dry-run');
|
|
2788
|
+
let addonScope;
|
|
2789
|
+
try {
|
|
2790
|
+
addonScope = detectScope(remainingArgs);
|
|
2791
|
+
if (addonScope === 'project' && remainingArgs.includes('--user'))
|
|
2792
|
+
addonScope = 'user';
|
|
2793
|
+
}
|
|
2794
|
+
catch (error) {
|
|
2795
|
+
return { exitCode: 1, message: `Error: ${error instanceof Error ? error.message : String(error)}` };
|
|
2796
|
+
}
|
|
2797
|
+
if (addonScope === 'user' && !USER_SCOPE_PATHS[provider]) {
|
|
2798
|
+
return {
|
|
2799
|
+
exitCode: 1,
|
|
2800
|
+
message: `--scope user not supported for provider '${provider}' — see docs/customization/user-scope-deployment.md for the supported list`,
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2708
2803
|
const runner = createScriptRunner(frameworkRoot);
|
|
2709
2804
|
const addonBaseArgs = ['--deploy-commands', '--deploy-skills', '--deploy-rules'];
|
|
2710
2805
|
// An explicitly selected upstream addon must be self-contained in the
|
|
@@ -2797,6 +2892,14 @@ export class UseHandler {
|
|
|
2797
2892
|
};
|
|
2798
2893
|
}
|
|
2799
2894
|
}
|
|
2895
|
+
if (!dryRunAddon && provider === 'omp' && (detectScope(remainingArgs) === 'user' || remainingArgs.includes('--user'))) {
|
|
2896
|
+
try {
|
|
2897
|
+
await deployOmpUserSource({ frameworkRoot, source: addonSource, target, bundle: framework, copyAll: true });
|
|
2898
|
+
}
|
|
2899
|
+
catch (error) {
|
|
2900
|
+
return { exitCode: 1, message: `OMP user deployment failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2800
2903
|
// Register only artifacts actually written by a confirmed deployment.
|
|
2801
2904
|
if (!dryRunAddon) {
|
|
2802
2905
|
try {
|
|
@@ -2922,7 +3025,7 @@ export class UseHandler {
|
|
|
2922
3025
|
catch {
|
|
2923
3026
|
// Profile selection is optional — don't fail deployment
|
|
2924
3027
|
}
|
|
2925
|
-
if (framework === 'aiwg-utils' &&
|
|
3028
|
+
if (framework === 'aiwg-utils' && !['pi', 'omp'].includes(provider) && !remainingArgs.includes('--dry-run')) {
|
|
2926
3029
|
const wrapperValidation = await validateDeployedModelWrappers({
|
|
2927
3030
|
provider: normalizeProviderDefinitionId(provider) ?? provider,
|
|
2928
3031
|
target,
|
|
@@ -3025,7 +3128,7 @@ export class UseHandler {
|
|
|
3025
3128
|
? withProviderOverride(deployFilteredArgs, provider)
|
|
3026
3129
|
: deployFilteredArgs;
|
|
3027
3130
|
const bulkKernelOnly = framework === 'all'
|
|
3028
|
-
&&
|
|
3131
|
+
&& !['pi', 'omp'].includes(provider)
|
|
3029
3132
|
&& !remainingArgs.includes('--copy-all')
|
|
3030
3133
|
&& !remainingArgs.includes('--copy-standard-skills');
|
|
3031
3134
|
if (bulkKernelOnly)
|
|
@@ -3234,7 +3337,7 @@ export class UseHandler {
|
|
|
3234
3337
|
// Pi model selection/headless routing is delivered by #2151. Until that
|
|
3235
3338
|
// adapter exists, do not require model-wrapper artifacts that Pi cannot
|
|
3236
3339
|
// load; resource deployment remains independently valid.
|
|
3237
|
-
if (!dryRun && !skipUtils && !bulkKernelOnly &&
|
|
3340
|
+
if (!dryRun && !skipUtils && !bulkKernelOnly && !['pi', 'omp'].includes(provider)) {
|
|
3238
3341
|
const wrapperValidation = await validateDeployedModelWrappers({
|
|
3239
3342
|
provider,
|
|
3240
3343
|
target,
|
|
@@ -3424,7 +3527,16 @@ export class UseHandler {
|
|
|
3424
3527
|
// mirror, record the deploy in the per-user registry at
|
|
3425
3528
|
// ~/.aiwg/installed.json so `aiwg list --scope user` and `aiwg remove
|
|
3426
3529
|
// --scope user` can find it from any cwd.
|
|
3427
|
-
if (scope === 'user' && provider
|
|
3530
|
+
if (scope === 'user' && provider === 'omp' && !dryRun) {
|
|
3531
|
+
try {
|
|
3532
|
+
await deployOmpUserSource({ frameworkRoot, source: frameworkRoot, target, bundle: framework, mode,
|
|
3533
|
+
copyAll: remainingArgs.includes('--copy-all') || remainingArgs.includes('--copy-standard-skills') });
|
|
3534
|
+
}
|
|
3535
|
+
catch (error) {
|
|
3536
|
+
return { exitCode: 1, message: `OMP user deployment failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
3537
|
+
}
|
|
3538
|
+
}
|
|
3539
|
+
if (scope === 'user' && provider !== 'openhuman' && provider !== 'omp' && !dryRun) {
|
|
3428
3540
|
try {
|
|
3429
3541
|
const paths = getProviderPaths(provider);
|
|
3430
3542
|
const resolveProjectPath = (p) => !p ? '' : path.isAbsolute(p) ? p : path.join(target, p);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Per ADR-4 §2 path map. Per ADR-4 §1: `--scope user` and `--scope
|
|
10
10
|
* project` are mutually exclusive; default is `project`.
|
|
11
11
|
*/
|
|
12
|
+
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
14
|
import * as path from 'node:path';
|
|
14
15
|
import { resolveHermesHome, resolveHermesHomePath } from '../providers/hermes-home.js';
|
|
@@ -57,6 +58,12 @@ export const USER_SCOPE_PATHS = {
|
|
|
57
58
|
rules: '',
|
|
58
59
|
behaviors: '',
|
|
59
60
|
},
|
|
61
|
+
get omp() {
|
|
62
|
+
const { agentDir } = resolveOmpPaths();
|
|
63
|
+
return { agents: path.join(agentDir, 'agents'), skills: path.join(agentDir, 'skills'),
|
|
64
|
+
commands: path.join(agentDir, 'prompts'), rules: path.join(agentDir, 'rules'),
|
|
65
|
+
behaviors: path.join(agentDir, 'extensions') };
|
|
66
|
+
},
|
|
60
67
|
pi: {
|
|
61
68
|
// Pi's global resource root is configurable. Unlike project deployment,
|
|
62
69
|
// user-scope resources belong under the effective agent directory. Keep
|
|
@@ -89,6 +89,7 @@ export function resolveDelivery(delivery) {
|
|
|
89
89
|
* - unknown: conservative 4 default.
|
|
90
90
|
*/
|
|
91
91
|
export const PROVIDER_PARALLELISM_DEFAULTS = {
|
|
92
|
+
antigravity: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
|
|
92
93
|
claude: { max_parallel_subagents: 4, max_parallel_ralph_loops: 2, max_parallel_mc_missions: 4 },
|
|
93
94
|
codex: { max_parallel_subagents: 10, max_parallel_ralph_loops: 3, max_parallel_mc_missions: 6 },
|
|
94
95
|
copilot: { max_parallel_subagents: 10, max_parallel_ralph_loops: 3, max_parallel_mc_missions: 6 },
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { McpClientLike } from "../storage/backends/fortemi.js";
|
|
2
|
+
export declare const FORTEMI_DATASET_LIVE_CONTRACT: "aiwg.fortemi-dataset-live-qualification/v1";
|
|
3
|
+
export declare const FORTEMI_DATASET_CAPABILITIES_TOOL = "dataset_capabilities";
|
|
4
|
+
export declare const FORTEMI_DATASET_EXECUTE_TOOL = "dataset_execute";
|
|
5
|
+
export interface FortemiDatasetLiveReceipt {
|
|
6
|
+
contract: typeof FORTEMI_DATASET_LIVE_CONTRACT;
|
|
7
|
+
outcome: "pending" | "supported";
|
|
8
|
+
diagnostic: "CONFORMANCE_FORTEMI_DATASET_CONTRACT_UNAVAILABLE" | "CONFORMANCE_FORTEMI_DATASET_PREFLIGHT_SUPPORTED";
|
|
9
|
+
receiptDigest: string;
|
|
10
|
+
bindings: {
|
|
11
|
+
aiwgCommit: string;
|
|
12
|
+
endpointFingerprint: string;
|
|
13
|
+
toolSchemaDigest: string;
|
|
14
|
+
};
|
|
15
|
+
observed: {
|
|
16
|
+
serverName: string;
|
|
17
|
+
serverVersion: string;
|
|
18
|
+
};
|
|
19
|
+
namespace: string;
|
|
20
|
+
operations: Array<{
|
|
21
|
+
tool: string;
|
|
22
|
+
compatible: boolean;
|
|
23
|
+
code: string;
|
|
24
|
+
}>;
|
|
25
|
+
mutation: {
|
|
26
|
+
authorized: false;
|
|
27
|
+
attempted: false;
|
|
28
|
+
};
|
|
29
|
+
resources: {
|
|
30
|
+
maxDurationMs: number;
|
|
31
|
+
durationMs: number;
|
|
32
|
+
maxToolCount: number;
|
|
33
|
+
observedToolCount: number;
|
|
34
|
+
maxSchemaBytes: number;
|
|
35
|
+
observedSchemaBytes: number;
|
|
36
|
+
networkAttempts: number;
|
|
37
|
+
toolCalls: number;
|
|
38
|
+
};
|
|
39
|
+
startedAt: string;
|
|
40
|
+
endedAt: string;
|
|
41
|
+
}
|
|
42
|
+
/** Read-only discovery. It never invokes a Fortemi tool, even when the proposed contract is present. */
|
|
43
|
+
export declare function qualifyFortemiDatasetLivePreflight(input: {
|
|
44
|
+
client: McpClientLike;
|
|
45
|
+
endpointUrl: string;
|
|
46
|
+
aiwgCommit: string;
|
|
47
|
+
maxDurationMs?: number;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
}): Promise<FortemiDatasetLiveReceipt>;
|
|
50
|
+
export declare function verifyFortemiDatasetLiveReceipt(value: unknown): string[];
|
|
51
|
+
/** Atomically creates private evidence without replacing an existing receipt. */
|
|
52
|
+
export declare function writeFortemiDatasetLiveReceipt(path: string, receipt: FortemiDatasetLiveReceipt): Promise<void>;
|
|
53
|
+
//# sourceMappingURL=fortemi-live-qualification.d.ts.map
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { endpointFingerprint, fortemiReceiptDigest, } from "../storage/fortemi-qualification-receipt.js";
|
|
5
|
+
export const FORTEMI_DATASET_LIVE_CONTRACT = "aiwg.fortemi-dataset-live-qualification/v1";
|
|
6
|
+
export const FORTEMI_DATASET_CAPABILITIES_TOOL = "dataset_capabilities";
|
|
7
|
+
export const FORTEMI_DATASET_EXECUTE_TOOL = "dataset_execute";
|
|
8
|
+
const UUID_NAMESPACE = /^aiwg-dataset-qualification-[0-9a-f-]{36}$/u;
|
|
9
|
+
const COMMIT = /^[0-9a-f]{40}$/u;
|
|
10
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/u;
|
|
11
|
+
const SAFE = /^[A-Za-z0-9._:/-]+$/u;
|
|
12
|
+
const REQUIRED_TOOLS = [
|
|
13
|
+
FORTEMI_DATASET_CAPABILITIES_TOOL,
|
|
14
|
+
FORTEMI_DATASET_EXECUTE_TOOL,
|
|
15
|
+
];
|
|
16
|
+
const MAX_TOOL_COUNT = 256;
|
|
17
|
+
const MAX_SCHEMA_BYTES = 1_048_576;
|
|
18
|
+
function record(value) {
|
|
19
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: undefined;
|
|
22
|
+
}
|
|
23
|
+
function exactKeys(value, allowed) {
|
|
24
|
+
const keys = Object.keys(value);
|
|
25
|
+
return (keys.length === allowed.length && keys.every((key) => allowed.includes(key)));
|
|
26
|
+
}
|
|
27
|
+
function material(receipt) {
|
|
28
|
+
const { receiptDigest: _digest, ...rest } = receipt;
|
|
29
|
+
return rest;
|
|
30
|
+
}
|
|
31
|
+
function inputSchemaCompatible(schema, required) {
|
|
32
|
+
const candidate = record(schema);
|
|
33
|
+
const properties = record(candidate?.properties);
|
|
34
|
+
const declaredRequired = candidate?.required;
|
|
35
|
+
if (candidate?.type !== "object" ||
|
|
36
|
+
!properties ||
|
|
37
|
+
!Array.isArray(declaredRequired))
|
|
38
|
+
return false;
|
|
39
|
+
if (!declaredRequired.every((key) => typeof key === "string") ||
|
|
40
|
+
new Set(declaredRequired).size !== declaredRequired.length)
|
|
41
|
+
return false;
|
|
42
|
+
return Object.entries(required).every(([key, type]) => {
|
|
43
|
+
const property = record(properties[key]);
|
|
44
|
+
return declaredRequired.includes(key) && property?.type === type;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
async function bounded(promise, timeoutMs) {
|
|
48
|
+
let timer;
|
|
49
|
+
return Promise.race([
|
|
50
|
+
promise,
|
|
51
|
+
new Promise((_, reject) => {
|
|
52
|
+
timer = setTimeout(() => reject(new Error("CONFORMANCE_FORTEMI_DATASET_PREFLIGHT_TIMEOUT")), timeoutMs);
|
|
53
|
+
}),
|
|
54
|
+
]).finally(() => {
|
|
55
|
+
if (timer)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Read-only discovery. It never invokes a Fortemi tool, even when the proposed contract is present. */
|
|
60
|
+
export async function qualifyFortemiDatasetLivePreflight(input) {
|
|
61
|
+
if (!COMMIT.test(input.aiwgCommit))
|
|
62
|
+
throw new Error("CONFORMANCE_INVALID_AIWG_COMMIT");
|
|
63
|
+
const requestedDuration = input.maxDurationMs ?? 5_000;
|
|
64
|
+
if (!Number.isFinite(requestedDuration))
|
|
65
|
+
throw new Error("CONFORMANCE_INVALID_RESOURCE_BOUND");
|
|
66
|
+
const maxDurationMs = Math.max(250, Math.min(Math.trunc(requestedDuration), 30_000));
|
|
67
|
+
const now = input.now ?? (() => new Date());
|
|
68
|
+
const startedAt = now().toISOString();
|
|
69
|
+
const namespace = `aiwg-dataset-qualification-${randomUUID()}`;
|
|
70
|
+
let schemas = [];
|
|
71
|
+
try {
|
|
72
|
+
if (!input.client.listTools)
|
|
73
|
+
throw new Error("CONFORMANCE_FORTEMI_TOOL_DISCOVERY_UNAVAILABLE");
|
|
74
|
+
const discovered = await bounded(input.client.listTools(), maxDurationMs);
|
|
75
|
+
schemas = discovered.tools ?? [];
|
|
76
|
+
if (!Array.isArray(schemas))
|
|
77
|
+
throw new Error("CONFORMANCE_FORTEMI_TOOL_INVENTORY_INVALID");
|
|
78
|
+
const inventory = schemas;
|
|
79
|
+
const observedSchemaBytes = Buffer.byteLength(JSON.stringify(inventory), "utf8");
|
|
80
|
+
if (inventory.length > MAX_TOOL_COUNT ||
|
|
81
|
+
observedSchemaBytes > MAX_SCHEMA_BYTES)
|
|
82
|
+
throw new Error("CONFORMANCE_RESOURCE_ENVELOPE_EXCEEDED");
|
|
83
|
+
const tools = new Map(inventory.map((tool) => [tool.name, tool]));
|
|
84
|
+
const specifications = [
|
|
85
|
+
{
|
|
86
|
+
tool: FORTEMI_DATASET_CAPABILITIES_TOOL,
|
|
87
|
+
required: { contract_version: "string" },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
tool: FORTEMI_DATASET_EXECUTE_TOOL,
|
|
91
|
+
required: {
|
|
92
|
+
contract_version: "string",
|
|
93
|
+
namespace: "string",
|
|
94
|
+
plan: "object",
|
|
95
|
+
records: "array",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
const checks = specifications.map(({ tool, required }) => {
|
|
100
|
+
const unique = inventory.filter((candidate) => candidate.name === tool).length === 1;
|
|
101
|
+
const compatible = unique && inputSchemaCompatible(tools.get(tool)?.inputSchema, required);
|
|
102
|
+
return {
|
|
103
|
+
tool,
|
|
104
|
+
compatible,
|
|
105
|
+
code: compatible
|
|
106
|
+
? "FORTEMI_DATASET_TOOL_SCHEMA_COMPATIBLE"
|
|
107
|
+
: tools.has(tool)
|
|
108
|
+
? "FORTEMI_DATASET_TOOL_SCHEMA_DRIFT"
|
|
109
|
+
: "FORTEMI_DATASET_TOOL_MISSING",
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const supported = checks.every((check) => check.compatible);
|
|
113
|
+
const endedAt = now().toISOString();
|
|
114
|
+
const server = input.client.serverVersion?.() ?? {};
|
|
115
|
+
const safeObserved = (value) => value && SAFE.test(value) ? value : "unreported";
|
|
116
|
+
const base = {
|
|
117
|
+
contract: FORTEMI_DATASET_LIVE_CONTRACT,
|
|
118
|
+
outcome: supported ? "supported" : "pending",
|
|
119
|
+
diagnostic: supported
|
|
120
|
+
? "CONFORMANCE_FORTEMI_DATASET_PREFLIGHT_SUPPORTED"
|
|
121
|
+
: "CONFORMANCE_FORTEMI_DATASET_CONTRACT_UNAVAILABLE",
|
|
122
|
+
bindings: {
|
|
123
|
+
aiwgCommit: input.aiwgCommit,
|
|
124
|
+
endpointFingerprint: endpointFingerprint(input.endpointUrl),
|
|
125
|
+
toolSchemaDigest: fortemiReceiptDigest(schemas),
|
|
126
|
+
},
|
|
127
|
+
observed: {
|
|
128
|
+
serverName: safeObserved(server.name),
|
|
129
|
+
serverVersion: safeObserved(server.version),
|
|
130
|
+
},
|
|
131
|
+
namespace,
|
|
132
|
+
operations: checks,
|
|
133
|
+
mutation: { authorized: false, attempted: false },
|
|
134
|
+
resources: {
|
|
135
|
+
maxDurationMs,
|
|
136
|
+
durationMs: Date.parse(endedAt) - Date.parse(startedAt),
|
|
137
|
+
maxToolCount: MAX_TOOL_COUNT,
|
|
138
|
+
observedToolCount: inventory.length,
|
|
139
|
+
maxSchemaBytes: MAX_SCHEMA_BYTES,
|
|
140
|
+
observedSchemaBytes,
|
|
141
|
+
networkAttempts: 1,
|
|
142
|
+
toolCalls: 0,
|
|
143
|
+
},
|
|
144
|
+
startedAt,
|
|
145
|
+
endedAt,
|
|
146
|
+
};
|
|
147
|
+
const receipt = { ...base, receiptDigest: fortemiReceiptDigest(base) };
|
|
148
|
+
const errors = verifyFortemiDatasetLiveReceipt(receipt);
|
|
149
|
+
if (errors.length)
|
|
150
|
+
throw new Error(errors.join(","));
|
|
151
|
+
return receipt;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await input.client.close?.();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export function verifyFortemiDatasetLiveReceipt(value) {
|
|
158
|
+
const errors = [];
|
|
159
|
+
const top = record(value);
|
|
160
|
+
if (!top)
|
|
161
|
+
return ["CONFORMANCE_RECEIPT_SHAPE_INVALID"];
|
|
162
|
+
const receipt = value;
|
|
163
|
+
if (!record(receipt.bindings) ||
|
|
164
|
+
!record(receipt.observed) ||
|
|
165
|
+
!record(receipt.mutation) ||
|
|
166
|
+
!record(receipt.resources) ||
|
|
167
|
+
!Array.isArray(receipt.operations) ||
|
|
168
|
+
!receipt.operations.every((item) => Boolean(record(item)))) {
|
|
169
|
+
return ["CONFORMANCE_RECEIPT_SHAPE_INVALID"];
|
|
170
|
+
}
|
|
171
|
+
if (!exactKeys(top, [
|
|
172
|
+
"contract",
|
|
173
|
+
"outcome",
|
|
174
|
+
"diagnostic",
|
|
175
|
+
"receiptDigest",
|
|
176
|
+
"bindings",
|
|
177
|
+
"observed",
|
|
178
|
+
"namespace",
|
|
179
|
+
"operations",
|
|
180
|
+
"mutation",
|
|
181
|
+
"resources",
|
|
182
|
+
"startedAt",
|
|
183
|
+
"endedAt",
|
|
184
|
+
]) ||
|
|
185
|
+
!exactKeys(receipt.bindings, [
|
|
186
|
+
"aiwgCommit",
|
|
187
|
+
"endpointFingerprint",
|
|
188
|
+
"toolSchemaDigest",
|
|
189
|
+
]) ||
|
|
190
|
+
!exactKeys(receipt.observed, [
|
|
191
|
+
"serverName",
|
|
192
|
+
"serverVersion",
|
|
193
|
+
]) ||
|
|
194
|
+
!exactKeys(receipt.mutation, [
|
|
195
|
+
"authorized",
|
|
196
|
+
"attempted",
|
|
197
|
+
]) ||
|
|
198
|
+
!exactKeys(receipt.resources, [
|
|
199
|
+
"maxDurationMs",
|
|
200
|
+
"durationMs",
|
|
201
|
+
"maxToolCount",
|
|
202
|
+
"observedToolCount",
|
|
203
|
+
"maxSchemaBytes",
|
|
204
|
+
"observedSchemaBytes",
|
|
205
|
+
"networkAttempts",
|
|
206
|
+
"toolCalls",
|
|
207
|
+
]) ||
|
|
208
|
+
!receipt.operations.every((item) => exactKeys(item, [
|
|
209
|
+
"tool",
|
|
210
|
+
"compatible",
|
|
211
|
+
"code",
|
|
212
|
+
])))
|
|
213
|
+
errors.push("CONFORMANCE_RECEIPT_SHAPE_INVALID");
|
|
214
|
+
if (receipt.contract !== FORTEMI_DATASET_LIVE_CONTRACT)
|
|
215
|
+
errors.push("CONFORMANCE_RECEIPT_CONTRACT_MISMATCH");
|
|
216
|
+
if (!COMMIT.test(receipt.bindings.aiwgCommit) ||
|
|
217
|
+
!DIGEST.test(receipt.bindings.endpointFingerprint) ||
|
|
218
|
+
!DIGEST.test(receipt.bindings.toolSchemaDigest))
|
|
219
|
+
errors.push("CONFORMANCE_RECEIPT_BINDING_INVALID");
|
|
220
|
+
if (!UUID_NAMESPACE.test(receipt.namespace))
|
|
221
|
+
errors.push("CONFORMANCE_RECEIPT_NAMESPACE_INVALID");
|
|
222
|
+
if (![
|
|
223
|
+
receipt.observed.serverName,
|
|
224
|
+
receipt.observed.serverVersion,
|
|
225
|
+
...receipt.operations.flatMap((item) => [item.tool, item.code]),
|
|
226
|
+
].every((value) => SAFE.test(value)))
|
|
227
|
+
errors.push("CONFORMANCE_RECEIPT_UNSAFE_VALUE");
|
|
228
|
+
if (receipt.mutation.authorized !== false ||
|
|
229
|
+
receipt.mutation.attempted !== false ||
|
|
230
|
+
receipt.resources.toolCalls !== 0 ||
|
|
231
|
+
receipt.resources.networkAttempts !== 1)
|
|
232
|
+
errors.push("CONFORMANCE_RECEIPT_MUTATION_INVALID");
|
|
233
|
+
const operationNames = receipt.operations.map((operation) => operation.tool);
|
|
234
|
+
const inventoryValid = receipt.operations.length === REQUIRED_TOOLS.length &&
|
|
235
|
+
REQUIRED_TOOLS.every((tool) => operationNames.filter((name) => name === tool).length === 1);
|
|
236
|
+
if (!inventoryValid ||
|
|
237
|
+
receipt.operations.some((operation) => operation.code !==
|
|
238
|
+
(operation.compatible
|
|
239
|
+
? "FORTEMI_DATASET_TOOL_SCHEMA_COMPATIBLE"
|
|
240
|
+
: operation.code === "FORTEMI_DATASET_TOOL_MISSING"
|
|
241
|
+
? operation.code
|
|
242
|
+
: "FORTEMI_DATASET_TOOL_SCHEMA_DRIFT")))
|
|
243
|
+
errors.push("CONFORMANCE_RECEIPT_OPERATION_INVALID");
|
|
244
|
+
const supported = inventoryValid &&
|
|
245
|
+
receipt.operations.every((operation) => operation.compatible);
|
|
246
|
+
const expectedDiagnostic = supported
|
|
247
|
+
? "CONFORMANCE_FORTEMI_DATASET_PREFLIGHT_SUPPORTED"
|
|
248
|
+
: "CONFORMANCE_FORTEMI_DATASET_CONTRACT_UNAVAILABLE";
|
|
249
|
+
if (receipt.outcome !== (supported ? "supported" : "pending") ||
|
|
250
|
+
receipt.diagnostic !== expectedDiagnostic)
|
|
251
|
+
errors.push("CONFORMANCE_RECEIPT_OUTCOME_INVALID");
|
|
252
|
+
const start = Date.parse(receipt.startedAt);
|
|
253
|
+
const end = Date.parse(receipt.endedAt);
|
|
254
|
+
if (!Number.isFinite(start) ||
|
|
255
|
+
!Number.isFinite(end) ||
|
|
256
|
+
end < start ||
|
|
257
|
+
receipt.resources.durationMs !== end - start)
|
|
258
|
+
errors.push("CONFORMANCE_RECEIPT_TIME_INVALID");
|
|
259
|
+
if (!Number.isInteger(receipt.resources.maxDurationMs) ||
|
|
260
|
+
receipt.resources.maxDurationMs < 250 ||
|
|
261
|
+
receipt.resources.maxDurationMs > 30_000 ||
|
|
262
|
+
!Number.isInteger(receipt.resources.durationMs) ||
|
|
263
|
+
receipt.resources.durationMs < 0 ||
|
|
264
|
+
receipt.resources.maxToolCount !== MAX_TOOL_COUNT ||
|
|
265
|
+
!Number.isInteger(receipt.resources.observedToolCount) ||
|
|
266
|
+
receipt.resources.observedToolCount < 0 ||
|
|
267
|
+
receipt.resources.observedToolCount > MAX_TOOL_COUNT ||
|
|
268
|
+
receipt.resources.maxSchemaBytes !== MAX_SCHEMA_BYTES ||
|
|
269
|
+
!Number.isInteger(receipt.resources.observedSchemaBytes) ||
|
|
270
|
+
receipt.resources.observedSchemaBytes < 0 ||
|
|
271
|
+
receipt.resources.observedSchemaBytes > MAX_SCHEMA_BYTES)
|
|
272
|
+
errors.push("CONFORMANCE_RECEIPT_RESOURCES_INVALID");
|
|
273
|
+
if (!DIGEST.test(receipt.receiptDigest) ||
|
|
274
|
+
receipt.receiptDigest !== fortemiReceiptDigest(material(receipt)))
|
|
275
|
+
errors.push("CONFORMANCE_RECEIPT_DIGEST_MISMATCH");
|
|
276
|
+
return errors;
|
|
277
|
+
}
|
|
278
|
+
/** Atomically creates private evidence without replacing an existing receipt. */
|
|
279
|
+
export async function writeFortemiDatasetLiveReceipt(path, receipt) {
|
|
280
|
+
const errors = verifyFortemiDatasetLiveReceipt(receipt);
|
|
281
|
+
if (errors.length)
|
|
282
|
+
throw new Error(errors.join(","));
|
|
283
|
+
await mkdir(dirname(path), { recursive: true });
|
|
284
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
285
|
+
await writeFile(temporary, `${JSON.stringify(receipt, null, 2)}\n`, {
|
|
286
|
+
encoding: "utf8",
|
|
287
|
+
mode: 0o600,
|
|
288
|
+
flag: "wx",
|
|
289
|
+
});
|
|
290
|
+
try {
|
|
291
|
+
await link(temporary, path);
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
await unlink(temporary).catch(() => undefined);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=fortemi-live-qualification.js.map
|
|
@@ -14,6 +14,7 @@ export * from './orchestration-repository.js';
|
|
|
14
14
|
export * from './file-orchestration-repository.js';
|
|
15
15
|
export * from './local-execution-backend.js';
|
|
16
16
|
export * from './fortemi-execution-bridge.js';
|
|
17
|
+
export * from './fortemi-live-qualification.js';
|
|
17
18
|
export * from './orchestration-service.js';
|
|
18
19
|
export * from './presentation.js';
|
|
19
20
|
export * from './conformance-types.js';
|
|
@@ -14,6 +14,7 @@ export * from './orchestration-repository.js';
|
|
|
14
14
|
export * from './file-orchestration-repository.js';
|
|
15
15
|
export * from './local-execution-backend.js';
|
|
16
16
|
export * from './fortemi-execution-bridge.js';
|
|
17
|
+
export * from './fortemi-live-qualification.js';
|
|
17
18
|
export * from './orchestration-service.js';
|
|
18
19
|
export * from './presentation.js';
|
|
19
20
|
export * from './conformance-types.js';
|