@aiwg/cli 2026.9.2 → 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.
Files changed (67) hide show
  1. package/agentic/code/providers/antigravity/provider-contract.v1.json +121 -0
  2. package/agentic/code/providers/capability-matrix.yaml +87 -1
  3. package/agentic/code/providers/model-capabilities.v1.json +31 -0
  4. package/agentic/code/providers/model-catalog.v1.json +29 -0
  5. package/agentic/code/providers/omp/README.md +58 -0
  6. package/agentic/code/providers/omp/aiwg-bridge.ts +52 -0
  7. package/dist/src/agents/agent-deployer.js +18 -0
  8. package/dist/src/agents/agent-packager.js +25 -0
  9. package/dist/src/artifacts/backends/sqlite-backend.js +18 -10
  10. package/dist/src/artifacts/query-engine.js +30 -0
  11. package/dist/src/cli/agent-spawn.js +13 -2
  12. package/dist/src/cli/handlers/help.js +3 -1
  13. package/dist/src/cli/handlers/init.js +2 -0
  14. package/dist/src/cli/handlers/models.js +1 -1
  15. package/dist/src/cli/handlers/runtime-info.js +8 -1
  16. package/dist/src/cli/handlers/session.js +5 -4
  17. package/dist/src/cli/handlers/sessions.js +26 -9
  18. package/dist/src/cli/handlers/setup.js +9 -2
  19. package/dist/src/cli/handlers/steward.js +13 -2
  20. package/dist/src/cli/handlers/subcommands.js +11 -0
  21. package/dist/src/cli/handlers/team.js +68 -7
  22. package/dist/src/cli/handlers/use.js +121 -9
  23. package/dist/src/cli/scope-resolver.js +7 -0
  24. package/dist/src/config/aiwg-config.js +1 -0
  25. package/dist/src/dataset/fortemi-live-qualification.d.ts +53 -0
  26. package/dist/src/dataset/fortemi-live-qualification.js +297 -0
  27. package/dist/src/dataset/index.d.ts +1 -0
  28. package/dist/src/dataset/index.js +1 -0
  29. package/dist/src/mcp/cli.mjs +30 -1
  30. package/dist/src/mcp/omp-config.mjs +128 -0
  31. package/dist/src/mcp/registry.js +45 -5
  32. package/dist/src/mcp/registry.mjs +29 -6
  33. package/dist/src/models/model-capabilities.v1.json +31 -0
  34. package/dist/src/models/model-catalog.v1.json +29 -0
  35. package/dist/src/models/model-discovery.js +46 -5
  36. package/dist/src/models/provider-policy.js +5 -3
  37. package/dist/src/plugin/skill-command-translator.js +2 -0
  38. package/dist/src/providers/capability-matrix.yaml +87 -1
  39. package/dist/src/providers/omp-agent.mjs +40 -0
  40. package/dist/src/providers/omp-diagnostics.mjs +15 -0
  41. package/dist/src/providers/omp-paths.mjs +38 -0
  42. package/dist/src/providers/provider-definitions.js +83 -0
  43. package/dist/src/providers/provider-definitions.mjs +25 -1
  44. package/dist/src/providers/provider-inventory.js +2 -0
  45. package/dist/src/sessions/adapters/omp.js +203 -0
  46. package/dist/src/sessions/batch-import.js +7 -0
  47. package/dist/src/sessions/contracts.js +1 -1
  48. package/dist/src/sessions/importer.js +4 -3
  49. package/dist/src/sessions/index.js +1 -0
  50. package/dist/src/sessions/readers.js +4 -3
  51. package/dist/src/sessions/workspace-discovery.js +12 -2
  52. package/dist/src/skills/deployer.js +21 -1
  53. package/dist/src/smiths/agentsmith/generator.js +1 -0
  54. package/dist/src/smiths/context-pipeline/parallelism-section.js +2 -0
  55. package/dist/src/smiths/context-pipeline/provider-policy.js +2 -2
  56. package/dist/src/smiths/context-pipeline/workspace-context.js +2 -2
  57. package/dist/src/storage/backends/fortemi.js +142 -17
  58. package/dist/src/storage/fortemi-qualification-receipt.js +206 -0
  59. package/dist/src/storage/fortemi-qualification.js +67 -6
  60. package/dist/src/storage/index.js +1 -0
  61. package/package.json +2 -1
  62. package/schemas/dataset/fortemi-live-qualification-receipt.v1.schema.json +132 -0
  63. package/tools/agents/deploy-agents.mjs +6 -3
  64. package/tools/agents/providers/antigravity.mjs +147 -0
  65. package/tools/agents/providers/omp.d.mts +4 -0
  66. package/tools/agents/providers/omp.mjs +256 -0
  67. package/tools/providers/antigravity-transport.mjs +124 -0
@@ -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';
@@ -18,6 +18,7 @@ import {
18
18
  } from './registry.mjs';
19
19
  import { McpProfileRegistry } from './profiles.mjs';
20
20
  import { getMcpInjectionDefinition } from '../providers/provider-definitions.mjs';
21
+ import { manageOmpMcp } from './omp-config.mjs';
21
22
 
22
23
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
24
 
@@ -38,6 +39,7 @@ Usage:
38
39
  aiwg mcp update <name> [opts] Update a server definition
39
40
  aiwg mcp list List registered MCP servers
40
41
  aiwg mcp inject [opts] Inject servers into provider configs
42
+ aiwg mcp uninject [opts] Remove unchanged AIWG-owned OMP server entries
41
43
  aiwg mcp profile <sub> Manage MCP profiles (named server subsets)
42
44
 
43
45
  Server Options (for add/update):
@@ -52,7 +54,8 @@ Server Options (for add/update):
52
54
  --description <text> Optional description
53
55
 
54
56
  Inject Options:
55
- --provider <name> Target provider (claude-code, cursor, factory, codex, opencode, windsurf, warp)
57
+ --provider <name> Target provider (including antigravity / agy and omp / oh-my-pi)
58
+ --scope <scope> project (default) or user; provider user scope must be documented
56
59
  --all Inject into all previously configured providers
57
60
  --servers <a,b,...> Only inject specific servers (comma-separated names)
58
61
  --dry-run Show what would change without writing
@@ -651,6 +654,8 @@ async function handleInject(args) {
651
654
  const serversStr = parseFlag(args, '--servers');
652
655
  const dryRun = args.includes('--dry-run');
653
656
  const projectDir = parseFlag(args, '--project') || '.';
657
+ const scope = parseFlag(args, '--scope') || 'project';
658
+ if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
654
659
  const profileName = parseFlag(args, '--profile');
655
660
  const ephemeral = args.includes('--ephemeral');
656
661
  const outPath = parseFlag(args, '--out');
@@ -782,10 +787,12 @@ async function handleInject(args) {
782
787
  servers: serverFilter,
783
788
  projectDir,
784
789
  dryRun,
790
+ scope,
785
791
  });
786
792
 
787
793
  if (result.error) {
788
794
  console.error(` ${p}: ${result.error}`);
795
+ process.exitCode = 1;
789
796
  continue;
790
797
  }
791
798
 
@@ -1124,6 +1131,15 @@ export async function main(args = process.argv.slice(2)) {
1124
1131
  }
1125
1132
 
1126
1133
  case 'install': {
1134
+ if (['omp', 'oh-my-pi'].includes(args[1])) {
1135
+ const scope = parseFlag(args, '--scope') || 'project';
1136
+ if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
1137
+ const projectDir = parseFlag(args, '--project') || (args[2] && !args[2].startsWith('--') ? args[2] : '.');
1138
+ const configPath = getProviderConfigPath('omp', projectDir, { scope });
1139
+ const result = await manageOmpMcp(configPath, [{ name: 'aiwg', type: 'stdio', command: 'aiwg', args: ['mcp', 'serve'] }], { dryRun: args.includes('--dry-run') });
1140
+ console.log(JSON.stringify(result, null, 2));
1141
+ break;
1142
+ }
1127
1143
  // Parse install arguments (skip flags)
1128
1144
  const installArgs = args.slice(1).filter(a => !a.startsWith('--'));
1129
1145
  const target = installArgs[0] || 'claude';
@@ -1184,6 +1200,19 @@ export async function main(args = process.argv.slice(2)) {
1184
1200
  await handleInject(subArgs);
1185
1201
  break;
1186
1202
 
1203
+ case 'uninject': {
1204
+ const provider = parseFlag(subArgs, '--provider');
1205
+ if (!['omp', 'oh-my-pi'].includes(provider)) throw new Error('uninject currently supports --provider omp');
1206
+ const scope = parseFlag(subArgs, '--scope') || 'project';
1207
+ if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
1208
+ const remove = (parseFlag(subArgs, '--servers') || '').split(',').map(s => s.trim()).filter(Boolean);
1209
+ if (!remove.length) throw new Error('uninject requires --servers name[,name]');
1210
+ const configPath = getProviderConfigPath(provider, parseFlag(subArgs, '--project') || '.', { scope });
1211
+ const result = await manageOmpMcp(configPath, [], { remove, dryRun: subArgs.includes('--dry-run') });
1212
+ console.log(JSON.stringify(result, null, 2));
1213
+ break;
1214
+ }
1215
+
1187
1216
  case 'profile':
1188
1217
  await handleProfile(subArgs);
1189
1218
  break;
@@ -0,0 +1,128 @@
1
+ import { readFile, writeFile, mkdir, rename, lstat, unlink, open } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+
5
+ const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
6
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value);
7
+ async function rejectSymlinkPath(file) {
8
+ let current = resolve(file);
9
+ for (;;) {
10
+ try {
11
+ if ((await lstat(current)).isSymbolicLink()) throw new Error('OMP MCP configuration path cannot traverse a symbolic link');
12
+ } catch (error) { if (error.code !== 'ENOENT') throw error; }
13
+ const parent = dirname(current);
14
+ if (parent === current) return;
15
+ current = parent;
16
+ }
17
+ }
18
+ async function readObject(file) {
19
+ await rejectSymlinkPath(file);
20
+ try {
21
+ if ((await lstat(file)).isSymbolicLink()) throw new Error('OMP MCP configuration cannot be a symbolic link');
22
+ const data = JSON.parse(await readFile(file, 'utf8'));
23
+ if (!record(data)) throw new Error('OMP MCP configuration must be an object');
24
+ return data;
25
+ } catch (error) {
26
+ if (error.code === 'ENOENT') return {};
27
+ if (error instanceof SyntaxError) throw new Error('OMP MCP configuration is invalid JSON; repair it before injection');
28
+ throw error;
29
+ }
30
+ }
31
+ async function atomic(file, data) {
32
+ await mkdir(dirname(file), { recursive: true });
33
+ const temporary = `${file}.${randomUUID()}.tmp`;
34
+ try {
35
+ await writeFile(temporary, JSON.stringify(data, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
36
+ await rename(temporary, file);
37
+ } finally { await unlink(temporary).catch(() => {}); }
38
+ }
39
+
40
+ export function ompServerConfig(server) {
41
+ const config = {};
42
+ for (const field of ['command', 'args', 'env', 'cwd', 'url', 'headers', 'enabled', 'timeout', 'requestIdFormat', 'auth', 'oauth', 'type']) {
43
+ if (server[field] !== undefined) config[field] = server[field];
44
+ }
45
+ if (!config.command && !config.url && config.enabled !== false) throw new Error(`OMP MCP server ${server.name} needs command or url`);
46
+ if (config.type !== undefined && !['stdio', 'http', 'sse'].includes(config.type)) throw new Error('Invalid OMP MCP transport');
47
+ if (config.command && config.url) throw new Error('OMP MCP server must choose command or url');
48
+ if (config.enabled !== false && (config.type === 'stdio' && !config.command || ['http', 'sse'].includes(config.type) && !config.url)) throw new Error('OMP MCP transport does not match endpoint');
49
+ for (const field of ['command', 'cwd', 'url']) {
50
+ if (config[field] !== undefined && typeof config[field] !== 'string') throw new Error(`Invalid OMP MCP ${field}`);
51
+ }
52
+ if (config.args !== undefined && (!Array.isArray(config.args) || config.args.some(x => typeof x !== 'string'))) throw new Error('Invalid OMP MCP args');
53
+ for (const field of ['env', 'headers']) {
54
+ if (config[field] !== undefined && (!record(config[field]) || Object.values(config[field]).some(x => typeof x !== 'string'))) throw new Error(`Invalid OMP MCP ${field}`);
55
+ }
56
+ if (config.requestIdFormat !== undefined && !['string', 'number'].includes(config.requestIdFormat)) throw new Error('Invalid OMP MCP requestIdFormat');
57
+ if (config.auth !== undefined && (!record(config.auth) || !['oauth', 'apikey'].includes(config.auth.type))) throw new Error('Invalid OMP MCP auth');
58
+ if (config.oauth !== undefined && !record(config.oauth)) throw new Error('Invalid OMP MCP oauth');
59
+ if (config.timeout !== undefined && (!Number.isFinite(config.timeout) || config.timeout < 0)) throw new Error('Invalid OMP MCP timeout');
60
+ if (config.enabled !== undefined && typeof config.enabled !== 'boolean') throw new Error('Invalid OMP MCP enabled flag');
61
+ for (const field of ['envPolicy', 'envLiteralKeys', 'headerPolicy']) {
62
+ if (server[field] !== undefined) throw new Error(`OMP native mcp.json does not enforce ${field}; this control requires the native SDK/plugin interface`);
63
+ }
64
+ for (const field of ['auth', 'oauth']) {
65
+ if (config[field]) for (const [key, value] of Object.entries(config[field])) {
66
+ if (key === 'callbackPort') { if (!Number.isInteger(value) || value < 0 || value > 65535) throw new Error('Invalid OMP MCP callbackPort'); }
67
+ else if (['type', 'credentialId', 'tokenUrl', 'clientId', 'clientSecret', 'resource', 'scope', 'redirectUri', 'callbackPath', 'prompt'].includes(key) && typeof value !== 'string') throw new Error(`Invalid OMP MCP ${field}.${key}`);
68
+ }
69
+ }
70
+ if (server.headerEnv) {
71
+ config.headers = { ...config.headers };
72
+ for (const [header, variable] of Object.entries(server.headerEnv)) {
73
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(variable)) throw new Error('Invalid MCP header environment reference');
74
+ config.headers[header] = '${' + variable + '}';
75
+ }
76
+ }
77
+ return config;
78
+ }
79
+
80
+ /** Hash-only receipt: never saves an extra copy of operator configuration. */
81
+ async function manageLockedOmpMcp(configPath, servers, { dryRun = false, remove = [] } = {}) {
82
+ const receiptPath = `${configPath}.aiwg-ownership.json`;
83
+ const existing = await readObject(configPath);
84
+ const receipt = await readObject(receiptPath);
85
+ if (receipt.servers !== undefined && (!record(receipt.servers) || receipt.schema !== 'aiwg.omp-mcp-ownership.v1')) throw new Error('Invalid OMP MCP ownership receipt');
86
+ if (existing.mcpServers !== undefined && !record(existing.mcpServers)) throw new Error('Invalid OMP mcpServers object');
87
+ const next = { ...(existing.mcpServers || {}) };
88
+ const owned = { ...(receipt.servers || {}) };
89
+ const result = { configPath, serversInjected: [], alreadyPresent: [], removed: [] };
90
+ // Validate all collisions before the first write.
91
+ for (const name of [...servers.map(s => s.name), ...remove]) {
92
+ if (typeof name !== 'string' || !name || ['__proto__', 'constructor', 'prototype'].includes(name)) throw new Error('Invalid OMP MCP server name');
93
+ if (Object.hasOwn(next, name) && owned[name] !== hash(next[name])) {
94
+ throw new Error(`OMP MCP server ${name} is operator-owned or modified; preserve it and choose a different name`);
95
+ }
96
+ }
97
+ for (const server of servers) {
98
+ if (!server.name || ['__proto__', 'constructor', 'prototype'].includes(server.name)) throw new Error('Invalid OMP MCP server name');
99
+ if (Object.hasOwn(next, server.name)) result.alreadyPresent.push(server.name);
100
+ next[server.name] = ompServerConfig(server);
101
+ owned[server.name] = hash(next[server.name]);
102
+ result.serversInjected.push(server.name);
103
+ }
104
+ for (const name of remove) {
105
+ if (owned[name]) { delete next[name]; delete owned[name]; result.removed.push(name); }
106
+ }
107
+ if (!dryRun) {
108
+ if (hash(await readObject(configPath)) !== hash(existing) || hash(await readObject(receiptPath)) !== hash(receipt)) throw new Error('OMP MCP configuration changed during injection; retry after reviewing the operator edit');
109
+ // If interrupted between writes, a retry fails closed on the hash mismatch.
110
+ await atomic(configPath, { ...existing, mcpServers: next });
111
+ await atomic(receiptPath, { schema: 'aiwg.omp-mcp-ownership.v1', servers: owned });
112
+ }
113
+ return result;
114
+ }
115
+
116
+ /** Serialize AIWG writers; operator edits detected before committing a replacement. */
117
+ export async function manageOmpMcp(configPath, servers, options = {}) {
118
+ if (options.dryRun) return manageLockedOmpMcp(configPath, servers, options);
119
+ await rejectSymlinkPath(configPath);
120
+ const lockPath = `${configPath}.aiwg-lock`;
121
+ await rejectSymlinkPath(lockPath);
122
+ await mkdir(dirname(configPath), { recursive: true });
123
+ let lock;
124
+ try { lock = await open(lockPath, 'wx', 0o600); }
125
+ catch (error) { if (error.code === 'EEXIST') throw new Error('OMP MCP injection already locked; wait for the active writer or review a stale lock before retrying'); throw error; }
126
+ try { return await manageLockedOmpMcp(configPath, servers, options); }
127
+ finally { await lock.close(); await unlink(lockPath); }
128
+ }