@aiwg/cli 2026.9.2 → 2026.9.4
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 +21 -0
- package/agentic/code/providers/antigravity/provider-contract.v1.json +121 -0
- package/agentic/code/providers/capability-matrix.yaml +87 -1
- package/agentic/code/providers/model-capabilities.v1.json +31 -0
- package/agentic/code/providers/model-catalog.v1.json +29 -0
- package/agentic/code/providers/omp/README.md +58 -0
- package/agentic/code/providers/omp/aiwg-bridge.ts +52 -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/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 +26 -9
- 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 +158 -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 +31 -0
- package/dist/src/models/model-catalog.v1.json +29 -0
- package/dist/src/models/model-discovery.js +46 -5
- package/dist/src/models/provider-policy.js +5 -3
- package/dist/src/plugin/skill-command-translator.js +2 -0
- package/dist/src/providers/capability-matrix.yaml +87 -1
- 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/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 +1 -0
- package/dist/src/sessions/readers.js +4 -3
- package/dist/src/sessions/workspace-discovery.js +12 -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/providers/antigravity-transport.mjs +124 -0
|
@@ -22,7 +22,7 @@ export class IncrementalSessionImporter {
|
|
|
22
22
|
this.repository = repository;
|
|
23
23
|
}
|
|
24
24
|
async import(request) {
|
|
25
|
-
assertSupportedSchemaMajor(request.source.sourceSchemaVersion);
|
|
25
|
+
assertSupportedSchemaMajor(request.source.sourceSchemaVersion, request.adapter.sourceSchemaMajor ?? 1);
|
|
26
26
|
if (request.inactivityThresholdMs !== undefined
|
|
27
27
|
&& (!Number.isFinite(request.inactivityThresholdMs) || request.inactivityThresholdMs < 0)) {
|
|
28
28
|
throw new SessionContractError('INVALID_ARGUMENT', 'session inactivity threshold must be a non-negative duration');
|
|
@@ -435,6 +435,7 @@ async function sourceContinuity(request, previous) {
|
|
|
435
435
|
selectedPath: request.selectedSource.locator,
|
|
436
436
|
allowedRoots: request.selectedSource.authorizedScope.allowedRoots,
|
|
437
437
|
};
|
|
438
|
+
const skipPrefixBytes = await request.adapter.mutablePrefixBytes?.(request.selectedSource) ?? 0;
|
|
438
439
|
const metadata = await fingerprintSourcePrefix(authorization, 0);
|
|
439
440
|
if (previous?.sourceSize !== undefined) {
|
|
440
441
|
if (metadata.size < previous.sourceSize) {
|
|
@@ -444,12 +445,12 @@ async function sourceContinuity(request, previous) {
|
|
|
444
445
|
&& previous.sourceFileIdentity !== metadata.fileIdentity) {
|
|
445
446
|
throw new SessionContractError('SCHEMA_DRIFT', 'session source file generation was replaced or rotated');
|
|
446
447
|
}
|
|
447
|
-
const priorPrefix = await fingerprintSourcePrefix(authorization, previous.sourceSize);
|
|
448
|
+
const priorPrefix = await fingerprintSourcePrefix(authorization, previous.sourceSize, skipPrefixBytes);
|
|
448
449
|
if (previous.prefixDigest && priorPrefix.digest !== previous.prefixDigest) {
|
|
449
450
|
throw new SessionContractError('SCHEMA_DRIFT', 'session source prefix was rewritten before its durable checkpoint');
|
|
450
451
|
}
|
|
451
452
|
}
|
|
452
|
-
const current = await fingerprintSourcePrefix(authorization, metadata.size);
|
|
453
|
+
const current = await fingerprintSourcePrefix(authorization, metadata.size, skipPrefixBytes);
|
|
453
454
|
const generation = previous?.sourceGeneration ?? sha256([
|
|
454
455
|
sourceGenerationDigest(request),
|
|
455
456
|
metadata.fileIdentity,
|
|
@@ -132,16 +132,17 @@ export async function fingerprintSourceFile(authorization) {
|
|
|
132
132
|
}
|
|
133
133
|
return { digest: `sha256:${hash.digest('hex')}`, size: allowed.size };
|
|
134
134
|
}
|
|
135
|
-
export async function fingerprintSourcePrefix(authorization, length) {
|
|
135
|
+
export async function fingerprintSourcePrefix(authorization, length, skipPrefixBytes = 0) {
|
|
136
136
|
const allowed = await authorizeSourceFile(authorization);
|
|
137
|
-
if (!Number.isSafeInteger(length) || length < 0 || length > allowed.size
|
|
137
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > allowed.size
|
|
138
|
+
|| !Number.isSafeInteger(skipPrefixBytes) || skipPrefixBytes < 0 || skipPrefixBytes > allowed.size) {
|
|
138
139
|
throw new SessionContractError('SCHEMA_DRIFT', 'checkpoint source position is beyond the current source size');
|
|
139
140
|
}
|
|
140
141
|
const handle = await open(allowed.canonicalPath, 'r');
|
|
141
142
|
const hash = createHash('sha256');
|
|
142
143
|
try {
|
|
143
144
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
144
|
-
let position =
|
|
145
|
+
let position = skipPrefixBytes;
|
|
145
146
|
while (position < length) {
|
|
146
147
|
const result = await handle.read(buffer, 0, Math.min(buffer.length, length - position), position);
|
|
147
148
|
if (result.bytesRead === 0)
|
|
@@ -7,6 +7,8 @@ import { ClaudeSessionAdapter } from './adapters/claude.js';
|
|
|
7
7
|
import { CodexSessionAdapter } from './adapters/codex.js';
|
|
8
8
|
import { CursorSessionAdapter } from './adapters/cursor.js';
|
|
9
9
|
import { FactorySessionAdapter } from './adapters/factory.js';
|
|
10
|
+
import { OmpSessionAdapter, readOmpSessionHeader } from './adapters/omp.js';
|
|
11
|
+
import { resolveOmpPaths } from '../providers/omp-paths.mjs';
|
|
10
12
|
import { PiSessionAdapter } from './adapters/pi.js';
|
|
11
13
|
import { SESSION_PROVIDER_IDS, sha256, } from './contracts.js';
|
|
12
14
|
import { redactSourceLocator } from './discovery.js';
|
|
@@ -23,6 +25,9 @@ export async function discoverWorkspaceHistories(options) {
|
|
|
23
25
|
const keyWithLeadingDash = workspaceKey(workspacePath, true);
|
|
24
26
|
const keyWithoutLeadingDash = workspaceKey(workspacePath, false);
|
|
25
27
|
const discoverable = [
|
|
28
|
+
{ provider: 'omp', adapter: new OmpSessionAdapter(), roots: options.ompRoot
|
|
29
|
+
? [resolve(options.ompRoot)] : options.providerHome
|
|
30
|
+
? [resolveOmpPaths({ home: resolve(options.providerHome), cwd: workspacePath }).sessionsDir] : [] },
|
|
26
31
|
{
|
|
27
32
|
provider: 'claude',
|
|
28
33
|
adapter: new ClaudeSessionAdapter(),
|
|
@@ -70,12 +75,12 @@ export async function discoverWorkspaceHistories(options) {
|
|
|
70
75
|
availableRoots.push(await canonicalPath(root));
|
|
71
76
|
}
|
|
72
77
|
if (availableRoots.length === 0) {
|
|
73
|
-
const codexNeedsAuthorization = entry.provider === 'codex'
|
|
78
|
+
const codexNeedsAuthorization = (entry.provider === 'codex' || entry.provider === 'omp')
|
|
74
79
|
&& entry.roots.length === 0;
|
|
75
80
|
reports.set(entry.provider, providerReport(entry.provider, codexNeedsAuthorization ? 'export-required' : 'unavailable', codexNeedsAuthorization ? 'manual-export' : 'discoverable', [], codexNeedsAuthorization
|
|
76
81
|
? 'SHARED_ROOT_AUTHORIZATION_REQUIRED'
|
|
77
82
|
: 'PROVIDER_ROOT_UNAVAILABLE', codexNeedsAuthorization
|
|
78
|
-
?
|
|
83
|
+
? `Pass --${entry.provider}-root with an explicitly authorized sessions root.`
|
|
79
84
|
: `No authorized ${entry.provider} workspace history root was found.`));
|
|
80
85
|
continue;
|
|
81
86
|
}
|
|
@@ -89,6 +94,9 @@ export async function discoverWorkspaceHistories(options) {
|
|
|
89
94
|
if (entry.provider === 'codex'
|
|
90
95
|
&& !await codexSourceMatchesWorkspace(locator, workspacePath))
|
|
91
96
|
continue;
|
|
97
|
+
const ompHeader = entry.provider === 'omp' ? await readOmpSessionHeader({ ...descriptor, sourceId: 'discovery', authorizedScope: scope }) : undefined;
|
|
98
|
+
if (ompHeader && resolve(ompHeader.cwd) !== workspacePath)
|
|
99
|
+
continue;
|
|
92
100
|
const details = await stat(locator);
|
|
93
101
|
const authorizedRoot = scope.allowedRoots.find((root) => locator === root || locator.startsWith(`${root}/`));
|
|
94
102
|
if (!authorizedRoot)
|
|
@@ -102,6 +110,8 @@ export async function discoverWorkspaceHistories(options) {
|
|
|
102
110
|
continue;
|
|
103
111
|
seen.add(dedupeKey);
|
|
104
112
|
const source = sourceFromDescriptor(descriptor, locator, authorizedRoot, details, fingerprint.digest);
|
|
113
|
+
if (ompHeader)
|
|
114
|
+
source.sourceId = sha256(['omp-native-source-v1', workspaceId, ompHeader.id].join('\0'));
|
|
105
115
|
providerSources.push(source);
|
|
106
116
|
candidates.push(source);
|
|
107
117
|
}
|
|
@@ -289,6 +289,16 @@ function readDeploymentSidecar(targetPath, expectedName, expectedProvider) {
|
|
|
289
289
|
return undefined;
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Codex and Antigravity intentionally consume the same portable project skill
|
|
294
|
+
* surface. A projection written for either provider is managed ownership for
|
|
295
|
+
* the other when the desired payload is otherwise byte-identical.
|
|
296
|
+
*/
|
|
297
|
+
function providersShareProjectionSurface(actual, expected) {
|
|
298
|
+
return actual === expected
|
|
299
|
+
|| (actual === 'codex' && expected === 'antigravity')
|
|
300
|
+
|| (actual === 'antigravity' && expected === 'codex');
|
|
301
|
+
}
|
|
292
302
|
function buildProjectionPlan(record, options) {
|
|
293
303
|
const policy = resolvePolicy(options.target, options);
|
|
294
304
|
const targetPath = path.join(policy.root, record.name);
|
|
@@ -339,12 +349,15 @@ function isManagedTarget(targetPath, expectedName, expectedProvider) {
|
|
|
339
349
|
try {
|
|
340
350
|
const targetStat = fs.lstatSync(targetPath);
|
|
341
351
|
const marker = path.join(targetPath, AGENT_SKILL_MANAGED_MARKER);
|
|
352
|
+
const sidecar = readDeploymentSidecar(targetPath, expectedName);
|
|
342
353
|
return (targetStat.isDirectory()
|
|
343
354
|
&& !targetStat.isSymbolicLink()
|
|
344
355
|
&& fs.lstatSync(marker).isFile()
|
|
345
356
|
&& !fs.lstatSync(marker).isSymbolicLink()
|
|
346
357
|
&& fs.readFileSync(marker, 'utf8') === MARKER_CONTENT
|
|
347
|
-
&&
|
|
358
|
+
&& sidecar !== undefined
|
|
359
|
+
&& (expectedProvider === undefined
|
|
360
|
+
|| providersShareProjectionSurface(sidecar.provider, expectedProvider)));
|
|
348
361
|
}
|
|
349
362
|
catch {
|
|
350
363
|
return false;
|
|
@@ -381,6 +394,13 @@ function targetMatches(targetPath, desired, name, provider) {
|
|
|
381
394
|
if (actual.size !== desired.length)
|
|
382
395
|
return false;
|
|
383
396
|
return desired.every((entry) => {
|
|
397
|
+
if (entry.kind === 'file'
|
|
398
|
+
&& entry.relativePath === AGENT_SKILL_DEPLOYMENT_SIDECAR) {
|
|
399
|
+
const sidecar = readDeploymentSidecar(targetPath, name);
|
|
400
|
+
return sidecar !== undefined
|
|
401
|
+
&& providersShareProjectionSurface(sidecar.provider, provider)
|
|
402
|
+
&& sidecar.sourceDigest === JSON.parse(entry.bytes.toString('utf8')).sourceDigest;
|
|
403
|
+
}
|
|
384
404
|
const value = actual.get(entry.relativePath);
|
|
385
405
|
return entry.kind === 'directory'
|
|
386
406
|
? value === 'directory'
|
|
@@ -18,6 +18,8 @@ export const PARALLELISM_BLOCK_START = '<!-- AIWG-PARALLELISM-CAP:START -->';
|
|
|
18
18
|
export const PARALLELISM_BLOCK_END = '<!-- AIWG-PARALLELISM-CAP:END -->';
|
|
19
19
|
function delegationSupport(provider) {
|
|
20
20
|
switch (provider) {
|
|
21
|
+
case 'antigravity':
|
|
22
|
+
return '**Provider behavior (antigravity)**: AIWG team execution uses aiwg-mc emulation. The conservative cap is four workers; native Antigravity subagent limits and heterogeneous model pinning remain unqualified.';
|
|
21
23
|
case 'claude':
|
|
22
24
|
case 'codex':
|
|
23
25
|
case 'copilot':
|
|
@@ -46,8 +46,8 @@ export function shouldEmitAiwgMd(provider) {
|
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
48
|
* Whether to emit AGENTS.md at project root. True for the AGENTS-MD
|
|
49
|
-
* provider set (codex, copilot, cursor, windsurf, hermes, warp,
|
|
50
|
-
* opencode). False for claude (uses CLAUDE.md hook instead), openclaw,
|
|
49
|
+
* provider set (antigravity, codex, copilot, cursor, windsurf, hermes, warp,
|
|
50
|
+
* factory, opencode). False for claude (uses CLAUDE.md hook instead), openclaw,
|
|
51
51
|
* and generic.
|
|
52
52
|
*/
|
|
53
53
|
export function shouldEmitAgentsMd(provider) {
|
|
@@ -430,8 +430,8 @@ export function buildProviderBootstrapBlock(provider) {
|
|
|
430
430
|
? [
|
|
431
431
|
'Load the canonical project context first, then the generated AIWG framework context:',
|
|
432
432
|
'',
|
|
433
|
-
'@WORKSPACE.md',
|
|
434
|
-
'@AIWG.md',
|
|
433
|
+
provider === 'omp' ? '@../WORKSPACE.md' : '@WORKSPACE.md',
|
|
434
|
+
provider === 'omp' ? '@../AIWG.md' : '@AIWG.md',
|
|
435
435
|
]
|
|
436
436
|
: contract.loadMode === 'config-registration'
|
|
437
437
|
? [
|
|
@@ -36,6 +36,45 @@
|
|
|
36
36
|
* @issue #961
|
|
37
37
|
* @issue #972
|
|
38
38
|
*/
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
const FORTEMI_QUERY_LIMIT = 50;
|
|
41
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
42
|
+
function schemaProperties(tool) {
|
|
43
|
+
const properties = tool?.inputSchema?.properties;
|
|
44
|
+
return properties && typeof properties === 'object'
|
|
45
|
+
? properties
|
|
46
|
+
: {};
|
|
47
|
+
}
|
|
48
|
+
/** Select an argument contract from discovered capabilities, never a version string. */
|
|
49
|
+
export function resolveFortemiToolProfile(tools) {
|
|
50
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
51
|
+
const legacy = schemaProperties(byName.get('get_note'));
|
|
52
|
+
if ('note_id' in legacy)
|
|
53
|
+
return 'legacy-note-id';
|
|
54
|
+
const currentGet = schemaProperties(byName.get('get_note'));
|
|
55
|
+
const currentUpsert = schemaProperties(byName.get('upsert_external_notes'));
|
|
56
|
+
const currentItems = currentUpsert.items;
|
|
57
|
+
if ('id' in currentGet &&
|
|
58
|
+
'source_namespace' in currentUpsert &&
|
|
59
|
+
'items' in currentUpsert &&
|
|
60
|
+
currentItems?.items?.properties &&
|
|
61
|
+
'external_id' in currentItems.items.properties &&
|
|
62
|
+
'content' in currentItems.items.properties &&
|
|
63
|
+
'caller_stable_id' in currentItems.items.properties)
|
|
64
|
+
return 'source-addressed-v1';
|
|
65
|
+
throw new Error('storage(fortemi): unsupported live MCP tool contract');
|
|
66
|
+
}
|
|
67
|
+
/** Stable UUID used as the opaque Fortemi handle for a subsystem/path identity. */
|
|
68
|
+
export function fortemiStableNoteId(subsystem, path) {
|
|
69
|
+
const bytes = createHash('sha256')
|
|
70
|
+
.update(`aiwg-storage\0${subsystem}\0${path}`)
|
|
71
|
+
.digest()
|
|
72
|
+
.subarray(0, 16);
|
|
73
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
74
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
75
|
+
const hex = bytes.toString('hex');
|
|
76
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
77
|
+
}
|
|
39
78
|
const DEFAULT_MCP_SERVER = 'fortemi';
|
|
40
79
|
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
41
80
|
export function resolveMcpRequestHeaders(server, environment = process.env) {
|
|
@@ -48,7 +87,8 @@ export function resolveMcpRequestHeaders(server, environment = process.env) {
|
|
|
48
87
|
if (!value) {
|
|
49
88
|
throw new Error(`storage(fortemi): required credential environment variable "${envName}" is not set`);
|
|
50
89
|
}
|
|
51
|
-
headers[header] =
|
|
90
|
+
headers[header] =
|
|
91
|
+
header.toLowerCase() === 'authorization' ? `Bearer ${value}` : value;
|
|
52
92
|
}
|
|
53
93
|
return headers;
|
|
54
94
|
}
|
|
@@ -75,6 +115,11 @@ export function unwrapMcpToolResult(result) {
|
|
|
75
115
|
?.filter((item) => item.type === 'text' && typeof item.text === 'string')
|
|
76
116
|
.map((item) => item.text)
|
|
77
117
|
.join('; ');
|
|
118
|
+
if (detail &&
|
|
119
|
+
/(?:API error 404|status(?: code)? 404)/i.test(detail) &&
|
|
120
|
+
/(?:Note not found|problems\/not-found)/i.test(detail)) {
|
|
121
|
+
return { not_found: true };
|
|
122
|
+
}
|
|
78
123
|
throw new Error(`storage(fortemi): MCP tool failed${detail ? `: ${detail}` : ''}`);
|
|
79
124
|
}
|
|
80
125
|
if (envelope.structuredContent !== undefined)
|
|
@@ -95,6 +140,7 @@ export class FortemiAdapter {
|
|
|
95
140
|
scheme;
|
|
96
141
|
clientFactory;
|
|
97
142
|
client = null;
|
|
143
|
+
profile = null;
|
|
98
144
|
constructor(opts) {
|
|
99
145
|
this.subsystem = opts.subsystem;
|
|
100
146
|
this.mcpServer = opts.config.mcpServer ?? DEFAULT_MCP_SERVER;
|
|
@@ -105,12 +151,21 @@ export class FortemiAdapter {
|
|
|
105
151
|
if (this.client)
|
|
106
152
|
return;
|
|
107
153
|
this.client = await this.clientFactory(this.mcpServer);
|
|
154
|
+
if (this.client.listTools) {
|
|
155
|
+
const discovered = await this.client.listTools();
|
|
156
|
+
this.profile = resolveFortemiToolProfile(discovered.tools ?? []);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Preserve injected/older clients which predate tool discovery.
|
|
160
|
+
this.profile = 'legacy-note-id';
|
|
161
|
+
}
|
|
108
162
|
}
|
|
109
163
|
async close() {
|
|
110
164
|
if (this.client?.close) {
|
|
111
165
|
await this.client.close();
|
|
112
166
|
}
|
|
113
167
|
this.client = null;
|
|
168
|
+
this.profile = null;
|
|
114
169
|
}
|
|
115
170
|
async getClient() {
|
|
116
171
|
if (!this.client)
|
|
@@ -132,17 +187,44 @@ export class FortemiAdapter {
|
|
|
132
187
|
async read(path) {
|
|
133
188
|
const id = this.noteId(path);
|
|
134
189
|
const client = await this.getClient();
|
|
135
|
-
const result = (await client.callTool('get_note',
|
|
190
|
+
const result = (await client.callTool('get_note', this.profile === 'source-addressed-v1'
|
|
191
|
+
? { id: fortemiStableNoteId(this.subsystem, path) }
|
|
192
|
+
: { note_id: id }));
|
|
136
193
|
if (!result || result.not_found)
|
|
137
194
|
return null;
|
|
138
|
-
const note = result.note;
|
|
195
|
+
const note = result.note ?? result;
|
|
139
196
|
if (!note)
|
|
140
197
|
return null;
|
|
141
|
-
return
|
|
198
|
+
return (result.revised?.content ??
|
|
199
|
+
result.original?.content ??
|
|
200
|
+
note.revised_content ??
|
|
201
|
+
note.content ??
|
|
202
|
+
null);
|
|
142
203
|
}
|
|
143
204
|
async write(path, content, meta) {
|
|
144
205
|
const id = this.noteId(path);
|
|
145
206
|
const client = await this.getClient();
|
|
207
|
+
if (this.profile === 'source-addressed-v1') {
|
|
208
|
+
const digest = createHash('sha256').update(content).digest('hex');
|
|
209
|
+
await client.callTool('upsert_external_notes', {
|
|
210
|
+
source_namespace: `aiwg.storage.${this.subsystem}`,
|
|
211
|
+
source_schema_version: 'aiwg.storage-entry/v1',
|
|
212
|
+
import_run_id: `sha256:${digest}`,
|
|
213
|
+
batch_id: `sha256:${digest}`,
|
|
214
|
+
policy: 'replace',
|
|
215
|
+
items: [
|
|
216
|
+
{
|
|
217
|
+
external_id: path,
|
|
218
|
+
content,
|
|
219
|
+
content_digest: `sha256:${digest}`,
|
|
220
|
+
caller_stable_id: fortemiStableNoteId(this.subsystem, path),
|
|
221
|
+
metadata: { ...this.buildMetadata(meta), aiwg_storage_path: path },
|
|
222
|
+
policy: 'replace',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
});
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
146
228
|
// Try update first; if not found, capture as new. Two calls in the
|
|
147
229
|
// worst case but idempotent — Fortemi's update_note increments the
|
|
148
230
|
// version rather than overwriting, which matches the Phase-4 design.
|
|
@@ -171,16 +253,29 @@ export class FortemiAdapter {
|
|
|
171
253
|
const subsystemPrefix = `${this.subsystem}:`;
|
|
172
254
|
const fullPrefix = prefix.length === 0 ? subsystemPrefix : `${subsystemPrefix}${prefix}`;
|
|
173
255
|
const result = (await client.callTool('list_notes', {
|
|
174
|
-
|
|
175
|
-
|
|
256
|
+
...(this.profile === 'source-addressed-v1'
|
|
257
|
+
? { limit: 500, offset: 0 }
|
|
258
|
+
: { id_prefix: fullPrefix, scheme: this.scheme }),
|
|
176
259
|
}));
|
|
177
260
|
const notes = result?.notes ?? [];
|
|
178
261
|
return notes
|
|
179
|
-
.
|
|
262
|
+
.map((n) => {
|
|
263
|
+
const current = n;
|
|
264
|
+
const path = current.metadata?.aiwg_storage_path;
|
|
265
|
+
return typeof path === 'string' &&
|
|
266
|
+
current.metadata?.subsystem === this.subsystem
|
|
267
|
+
? {
|
|
268
|
+
...n,
|
|
269
|
+
note_id: `${subsystemPrefix}${path}`,
|
|
270
|
+
external_id: current.id,
|
|
271
|
+
}
|
|
272
|
+
: n;
|
|
273
|
+
})
|
|
274
|
+
.filter((n) => typeof n.note_id === 'string' && n.note_id.startsWith(fullPrefix))
|
|
180
275
|
.map((n) => {
|
|
181
276
|
const entry = {
|
|
182
277
|
path: n.note_id.slice(subsystemPrefix.length),
|
|
183
|
-
externalId: n.note_id,
|
|
278
|
+
externalId: n.external_id ?? n.note_id,
|
|
184
279
|
};
|
|
185
280
|
if (typeof n.size === 'number')
|
|
186
281
|
entry.size = n.size;
|
|
@@ -200,11 +295,16 @@ export class FortemiAdapter {
|
|
|
200
295
|
// the note from list/read by archiving it).
|
|
201
296
|
const id = this.noteId(path);
|
|
202
297
|
const client = await this.getClient();
|
|
203
|
-
const
|
|
204
|
-
|
|
298
|
+
const identityArgs = this.profile === 'source-addressed-v1'
|
|
299
|
+
? { id: fortemiStableNoteId(this.subsystem, path) }
|
|
300
|
+
: { note_id: id };
|
|
301
|
+
const existing = (await client.callTool('get_note', identityArgs));
|
|
302
|
+
if (!existing ||
|
|
303
|
+
existing.not_found ||
|
|
304
|
+
(this.profile !== 'source-addressed-v1' && !existing.note))
|
|
205
305
|
return;
|
|
206
306
|
await client.callTool('update_note', {
|
|
207
|
-
|
|
307
|
+
...identityArgs,
|
|
208
308
|
archived: true,
|
|
209
309
|
});
|
|
210
310
|
}
|
|
@@ -212,16 +312,41 @@ export class FortemiAdapter {
|
|
|
212
312
|
const client = await this.getClient();
|
|
213
313
|
const subsystemPrefix = `${this.subsystem}:`;
|
|
214
314
|
const result = (await client.callTool('search', {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
315
|
+
...(this.profile === 'source-addressed-v1'
|
|
316
|
+
? { action: 'text', query: q, limit: FORTEMI_QUERY_LIMIT }
|
|
317
|
+
: { query: q, id_prefix: subsystemPrefix, scheme: this.scheme }),
|
|
218
318
|
}));
|
|
219
319
|
const results = result?.results ?? [];
|
|
220
|
-
|
|
221
|
-
|
|
320
|
+
const hydrated = [];
|
|
321
|
+
for (const resultItem of results.slice(0, FORTEMI_QUERY_LIMIT)) {
|
|
322
|
+
let item = resultItem;
|
|
323
|
+
if (this.profile === 'source-addressed-v1' &&
|
|
324
|
+
typeof item.id === 'string' &&
|
|
325
|
+
UUID.test(item.id) &&
|
|
326
|
+
(!item.metadata || typeof item.metadata.aiwg_storage_path !== 'string')) {
|
|
327
|
+
const detail = (await client.callTool('get_note', { id: item.id }));
|
|
328
|
+
if (!detail || detail.not_found)
|
|
329
|
+
continue;
|
|
330
|
+
const note = detail.note;
|
|
331
|
+
if (!note || note.id !== item.id)
|
|
332
|
+
continue;
|
|
333
|
+
item = { ...item, metadata: note.metadata };
|
|
334
|
+
}
|
|
335
|
+
hydrated.push(item);
|
|
336
|
+
}
|
|
337
|
+
return hydrated
|
|
338
|
+
.map((r) => {
|
|
339
|
+
const path = r.metadata?.aiwg_storage_path;
|
|
340
|
+
return typeof path === 'string' &&
|
|
341
|
+
r.metadata?.subsystem === this.subsystem
|
|
342
|
+
? { ...r, note_id: `${subsystemPrefix}${path}` }
|
|
343
|
+
: r;
|
|
344
|
+
})
|
|
345
|
+
.filter((r) => typeof r.note_id === 'string' &&
|
|
346
|
+
r.note_id.startsWith(subsystemPrefix))
|
|
222
347
|
.map((r) => ({
|
|
223
348
|
path: r.note_id.slice(subsystemPrefix.length),
|
|
224
|
-
externalId: r.note_id,
|
|
349
|
+
externalId: r.id ?? r.note_id,
|
|
225
350
|
}));
|
|
226
351
|
}
|
|
227
352
|
buildMetadata(meta) {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { link, mkdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
export const FORTEMI_QUALIFICATION_RECEIPT = "aiwg.fortemi-live-qualification-receipt/v1";
|
|
6
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
7
|
+
const COMMIT = /^[0-9a-f]{40}$/;
|
|
8
|
+
const REF = /^(?:refs\/(?:heads|tags)\/[A-Za-z0-9._/-]+|[0-9a-f]{40})$/;
|
|
9
|
+
const SHORT_REF = /^[A-Za-z0-9._/-]+$/;
|
|
10
|
+
const SAFE = /^[A-Za-z0-9._:/-]+$/;
|
|
11
|
+
const NAMESPACE = /^aiwg-qualification-[0-9a-f-]{36}$/;
|
|
12
|
+
const REQUIRED_OPERATIONS = new Set([
|
|
13
|
+
"read",
|
|
14
|
+
"write",
|
|
15
|
+
"update",
|
|
16
|
+
"list",
|
|
17
|
+
"query",
|
|
18
|
+
]);
|
|
19
|
+
function hasCompleteOperationInventory(operations) {
|
|
20
|
+
const names = operations.map((operation) => operation.operation);
|
|
21
|
+
return (names.length === REQUIRED_OPERATIONS.size &&
|
|
22
|
+
new Set(names).size === REQUIRED_OPERATIONS.size &&
|
|
23
|
+
names.every((name) => REQUIRED_OPERATIONS.has(name)));
|
|
24
|
+
}
|
|
25
|
+
export function resolveFortemiQualificationSource(env = process.env, cwd = process.cwd()) {
|
|
26
|
+
const git = (...args) => execFileSync("git", args, {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
}).trim();
|
|
31
|
+
const aiwgCommit = env.AIWG_STORAGE_QUALIFICATION_COMMIT || git("rev-parse", "HEAD");
|
|
32
|
+
const configuredRef = env.AIWG_STORAGE_QUALIFICATION_BRANCH;
|
|
33
|
+
const aiwgRef = configuredRef
|
|
34
|
+
? REF.test(configuredRef)
|
|
35
|
+
? configuredRef
|
|
36
|
+
: SHORT_REF.test(configuredRef) && !configuredRef.includes("..")
|
|
37
|
+
? `refs/heads/${configuredRef}`
|
|
38
|
+
: configuredRef
|
|
39
|
+
: (() => {
|
|
40
|
+
try {
|
|
41
|
+
return git("symbolic-ref", "-q", "HEAD");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return aiwgCommit;
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
if (!COMMIT.test(aiwgCommit))
|
|
48
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_COMMIT");
|
|
49
|
+
if (!REF.test(aiwgRef) || aiwgRef.includes(".."))
|
|
50
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_REF");
|
|
51
|
+
return { aiwgCommit, aiwgRef };
|
|
52
|
+
}
|
|
53
|
+
function canonical(value) {
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
56
|
+
if (value && typeof value === "object")
|
|
57
|
+
return `{${Object.entries(value)
|
|
58
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
59
|
+
.map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`)
|
|
60
|
+
.join(",")}}`;
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
}
|
|
63
|
+
export function fortemiReceiptDigest(value) {
|
|
64
|
+
return `sha256:${createHash("sha256").update(canonical(value)).digest("hex")}`;
|
|
65
|
+
}
|
|
66
|
+
export function endpointFingerprint(rawUrl) {
|
|
67
|
+
const url = new URL(rawUrl);
|
|
68
|
+
url.username = "";
|
|
69
|
+
url.password = "";
|
|
70
|
+
url.hash = "";
|
|
71
|
+
return fortemiReceiptDigest(url.toString());
|
|
72
|
+
}
|
|
73
|
+
function receiptMaterial(receipt) {
|
|
74
|
+
const { receiptDigest: _digest, ...material } = receipt;
|
|
75
|
+
return material;
|
|
76
|
+
}
|
|
77
|
+
export function createFortemiQualificationReceipt(input) {
|
|
78
|
+
if (!COMMIT.test(input.aiwgCommit))
|
|
79
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_COMMIT");
|
|
80
|
+
if (!REF.test(input.aiwgRef) || input.aiwgRef.includes(".."))
|
|
81
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_REF");
|
|
82
|
+
if (!input.report.server.name || !SAFE.test(input.report.server.name))
|
|
83
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_SERVER_NAME");
|
|
84
|
+
if (!input.report.server.version || !SAFE.test(input.report.server.version))
|
|
85
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_SERVER_VERSION");
|
|
86
|
+
if (!SAFE.test(input.contractRevision))
|
|
87
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_CONTRACT_REVISION");
|
|
88
|
+
if (!NAMESPACE.test(input.report.namespace))
|
|
89
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_NAMESPACE");
|
|
90
|
+
if (input.report.mutationAttempted !== Boolean(input.mutationObjectId))
|
|
91
|
+
throw new Error("FORTEMI_RECEIPT_MUTATION_BINDING_MISMATCH");
|
|
92
|
+
if (input.mutationObjectId && !SAFE.test(input.mutationObjectId))
|
|
93
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_OBJECT_ID");
|
|
94
|
+
const start = Date.parse(input.startedAt);
|
|
95
|
+
const end = Date.parse(input.endedAt);
|
|
96
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start)
|
|
97
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_TIMESTAMPS");
|
|
98
|
+
if (!Number.isInteger(input.timeoutMs) ||
|
|
99
|
+
input.timeoutMs < 250 ||
|
|
100
|
+
input.timeoutMs > 30_000 ||
|
|
101
|
+
!Number.isInteger(input.networkAttempts) ||
|
|
102
|
+
input.networkAttempts < 0)
|
|
103
|
+
throw new Error("FORTEMI_RECEIPT_INVALID_RESOURCES");
|
|
104
|
+
const operations = input.report.operations.map(({ operation, tool, compatible, code }) => {
|
|
105
|
+
if (![operation, tool, code].every((value) => SAFE.test(value)))
|
|
106
|
+
throw new Error("FORTEMI_RECEIPT_UNSAFE_OPERATION");
|
|
107
|
+
return { operation, tool, compatible, code };
|
|
108
|
+
});
|
|
109
|
+
if (!hasCompleteOperationInventory(operations))
|
|
110
|
+
throw new Error("FORTEMI_RECEIPT_OPERATION_INVENTORY_INVALID");
|
|
111
|
+
const material = {
|
|
112
|
+
contract: FORTEMI_QUALIFICATION_RECEIPT,
|
|
113
|
+
outcome: input.report.compatible ? "passed" : "failed",
|
|
114
|
+
bindings: {
|
|
115
|
+
aiwgCommit: input.aiwgCommit,
|
|
116
|
+
aiwgRef: input.aiwgRef,
|
|
117
|
+
endpointFingerprint: endpointFingerprint(input.endpointUrl),
|
|
118
|
+
toolSchemaDigest: fortemiReceiptDigest(input.toolSchemas),
|
|
119
|
+
},
|
|
120
|
+
observed: {
|
|
121
|
+
serverName: input.report.server.name,
|
|
122
|
+
serverVersion: input.report.server.version,
|
|
123
|
+
contractRevision: input.contractRevision,
|
|
124
|
+
},
|
|
125
|
+
namespace: input.report.namespace,
|
|
126
|
+
operations,
|
|
127
|
+
mutation: {
|
|
128
|
+
attempted: input.report.mutationAttempted,
|
|
129
|
+
...(input.mutationObjectId ? { objectId: input.mutationObjectId } : {}),
|
|
130
|
+
},
|
|
131
|
+
startedAt: new Date(start).toISOString(),
|
|
132
|
+
endedAt: new Date(end).toISOString(),
|
|
133
|
+
resources: {
|
|
134
|
+
timeoutMs: input.timeoutMs,
|
|
135
|
+
durationMs: end - start,
|
|
136
|
+
networkAttempts: input.networkAttempts,
|
|
137
|
+
toolCount: operations.length,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
return { ...material, receiptDigest: fortemiReceiptDigest(material) };
|
|
141
|
+
}
|
|
142
|
+
export function verifyFortemiQualificationReceipt(receipt) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
if (receipt.contract !== FORTEMI_QUALIFICATION_RECEIPT)
|
|
145
|
+
errors.push("FORTEMI_RECEIPT_CONTRACT_MISMATCH");
|
|
146
|
+
if (!["passed", "failed"].includes(receipt.outcome) ||
|
|
147
|
+
receipt.outcome !==
|
|
148
|
+
(receipt.operations.every((operation) => operation.compatible)
|
|
149
|
+
? "passed"
|
|
150
|
+
: "failed"))
|
|
151
|
+
errors.push("FORTEMI_RECEIPT_OUTCOME_INVALID");
|
|
152
|
+
if (!DIGEST.test(receipt.receiptDigest) ||
|
|
153
|
+
receipt.receiptDigest !== fortemiReceiptDigest(receiptMaterial(receipt)))
|
|
154
|
+
errors.push("FORTEMI_RECEIPT_DIGEST_MISMATCH");
|
|
155
|
+
if (!DIGEST.test(receipt.bindings.endpointFingerprint) ||
|
|
156
|
+
!DIGEST.test(receipt.bindings.toolSchemaDigest))
|
|
157
|
+
errors.push("FORTEMI_RECEIPT_BINDING_INVALID");
|
|
158
|
+
if (!COMMIT.test(receipt.bindings.aiwgCommit) ||
|
|
159
|
+
!REF.test(receipt.bindings.aiwgRef))
|
|
160
|
+
errors.push("FORTEMI_RECEIPT_SOURCE_INVALID");
|
|
161
|
+
if (!SAFE.test(receipt.observed.serverName) ||
|
|
162
|
+
!SAFE.test(receipt.observed.serverVersion) ||
|
|
163
|
+
!SAFE.test(receipt.observed.contractRevision) ||
|
|
164
|
+
!NAMESPACE.test(receipt.namespace))
|
|
165
|
+
errors.push("FORTEMI_RECEIPT_OBSERVATION_INVALID");
|
|
166
|
+
if (receipt.operations.some((item) => !SAFE.test(item.operation) ||
|
|
167
|
+
!SAFE.test(item.tool) ||
|
|
168
|
+
!SAFE.test(item.code)))
|
|
169
|
+
errors.push("FORTEMI_RECEIPT_OPERATION_INVALID");
|
|
170
|
+
if (!hasCompleteOperationInventory(receipt.operations))
|
|
171
|
+
errors.push("FORTEMI_RECEIPT_OPERATION_INVENTORY_INVALID");
|
|
172
|
+
if (receipt.mutation.attempted !== Boolean(receipt.mutation.objectId) ||
|
|
173
|
+
(receipt.mutation.objectId && !SAFE.test(receipt.mutation.objectId)))
|
|
174
|
+
errors.push("FORTEMI_RECEIPT_MUTATION_INVALID");
|
|
175
|
+
if (Date.parse(receipt.endedAt) < Date.parse(receipt.startedAt) ||
|
|
176
|
+
receipt.resources.durationMs !==
|
|
177
|
+
Date.parse(receipt.endedAt) - Date.parse(receipt.startedAt))
|
|
178
|
+
errors.push("FORTEMI_RECEIPT_TIME_INVALID");
|
|
179
|
+
if (!Number.isInteger(receipt.resources.timeoutMs) ||
|
|
180
|
+
receipt.resources.timeoutMs < 250 ||
|
|
181
|
+
receipt.resources.timeoutMs > 30_000 ||
|
|
182
|
+
!Number.isInteger(receipt.resources.networkAttempts) ||
|
|
183
|
+
receipt.resources.networkAttempts < 0 ||
|
|
184
|
+
receipt.resources.toolCount !== receipt.operations.length)
|
|
185
|
+
errors.push("FORTEMI_RECEIPT_RESOURCES_INVALID");
|
|
186
|
+
return errors;
|
|
187
|
+
}
|
|
188
|
+
export async function writeFortemiQualificationReceipt(path, receipt) {
|
|
189
|
+
const errors = verifyFortemiQualificationReceipt(receipt);
|
|
190
|
+
if (errors.length)
|
|
191
|
+
throw new Error(errors.join(","));
|
|
192
|
+
await mkdir(dirname(path), { recursive: true });
|
|
193
|
+
const temporary = `${path}.tmp-${process.pid}`;
|
|
194
|
+
await writeFile(temporary, `${JSON.stringify(receipt, null, 2)}\n`, {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
mode: 0o600,
|
|
197
|
+
flag: "wx",
|
|
198
|
+
});
|
|
199
|
+
try {
|
|
200
|
+
await link(temporary, path);
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
await unlink(temporary).catch(() => undefined);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
//# sourceMappingURL=fortemi-qualification-receipt.js.map
|