@aiwg/cli 2026.9.15 → 2026.9.16
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/capability-matrix.yaml +42 -0
- package/agentic/code/providers/model-capabilities.v1.json +119 -34
- package/agentic/code/providers/model-catalog.v1.json +259 -56
- package/dist/src/cli/handlers/sessions.js +159 -45
- package/dist/src/cli/handlers/use.js +20 -1
- package/dist/src/cli/scope-resolver.js +13 -0
- package/dist/src/cli/services/deployment-verification.js +33 -2
- package/dist/src/config/aiwg-config.js +39 -0
- package/dist/src/config/user-registry.js +9 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +119 -34
- package/dist/src/models/model-catalog.v1.json +259 -56
- package/dist/src/models/model-discovery.js +7 -0
- package/dist/src/models/provider-policy.js +1 -1
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +42 -0
- package/dist/src/providers/grokbot-paths.js +103 -0
- package/dist/src/providers/provider-definitions.js +85 -1
- package/dist/src/providers/provider-inventory.js +1 -0
- package/dist/src/sessions/adapters/grokbot.js +49 -0
- package/dist/src/sessions/adapters/hermes.js +17 -9
- package/dist/src/sessions/adapters/opencode.js +12 -1
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/export-plan.js +130 -4
- package/dist/src/sessions/fortemi-export-mapping.js +10 -6
- package/dist/src/sessions/fortemi-shard-blobs.js +78 -0
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/workspace-discovery.js +1 -1
- package/dist/src/skills/deployer.js +17 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/package.json +1 -1
- package/tools/agents/deploy-agents.mjs +3 -2
- package/tools/agents/providers/grokbot.mjs +285 -0
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { resolveHermesHomePath } from './hermes-home.js';
|
|
5
|
+
import { resolveGrokbotSkillsDir } from './grokbot-paths.js';
|
|
5
6
|
import { getProviderCapabilities, } from './capability-matrix.js';
|
|
6
7
|
const ArtifactPathsSchema = z.object({
|
|
7
8
|
agents: z.string().nullable(),
|
|
@@ -19,6 +20,7 @@ const ProviderDefinitionSchema = z.object({
|
|
|
19
20
|
'cursor',
|
|
20
21
|
'deepseek-harness',
|
|
21
22
|
'factory',
|
|
23
|
+
'grokbot',
|
|
22
24
|
'hermes',
|
|
23
25
|
'opencode',
|
|
24
26
|
'openclaw',
|
|
@@ -140,6 +142,7 @@ export const PROVIDER_IDS = [
|
|
|
140
142
|
'cursor',
|
|
141
143
|
'deepseek-harness',
|
|
142
144
|
'factory',
|
|
145
|
+
'grokbot',
|
|
143
146
|
'hermes',
|
|
144
147
|
'opencode',
|
|
145
148
|
'openclaw',
|
|
@@ -204,6 +207,17 @@ const CONTEXT_CONTRACTS = {
|
|
|
204
207
|
bootstrapTargets: ['AGENTS.md'], maxContextBytes: null, recommendedMaxLines: 150, nestedContext: true, support: 'supported',
|
|
205
208
|
verification: { method: 'official Factory AGENTS.md discovery hierarchy', source: 'https://docs.factory.ai/cli/configuration/agents-md', lastVerified: VERIFIED_ON },
|
|
206
209
|
},
|
|
210
|
+
grokbot: {
|
|
211
|
+
startupFiles: ['AGENTS.md'],
|
|
212
|
+
precedence: ['provider/system', 'project AGENTS.md when the host exposes it', 'explicit aiwg discover/show'],
|
|
213
|
+
loadMode: 'prose-directive', includeSyntax: null, configRegistration: null,
|
|
214
|
+
bootstrapTargets: ['AGENTS.md'], maxContextBytes: null, recommendedMaxLines: null, nestedContext: false, support: 'degraded',
|
|
215
|
+
verification: {
|
|
216
|
+
method: 'AIWG discover-first adapter contract; native Grok Bot startup/include path not yet verified — explicit-read guidance only',
|
|
217
|
+
source: 'docs/architecture/adr-grokbot-provider-target.md',
|
|
218
|
+
lastVerified: '2026-09-15',
|
|
219
|
+
},
|
|
220
|
+
},
|
|
207
221
|
hermes: {
|
|
208
222
|
startupFiles: ['.hermes.md', 'AGENTS.md'], precedence: ['provider/system', '.hermes.md', 'AGENTS.md'],
|
|
209
223
|
loadMode: 'prose-directive', includeSyntax: null, configRegistration: null,
|
|
@@ -556,6 +570,68 @@ const BUILT_IN_SEEDS = [
|
|
|
556
570
|
},
|
|
557
571
|
matrixRef: 'factory',
|
|
558
572
|
},
|
|
573
|
+
{
|
|
574
|
+
id: 'grokbot',
|
|
575
|
+
displayName: 'Grok Bot',
|
|
576
|
+
aliases: [],
|
|
577
|
+
// #210: promoted stable — Linux PUW + security + migration + maintainer Linux-only waiver 2026-09-16.
|
|
578
|
+
status: 'stable',
|
|
579
|
+
builtIn: true,
|
|
580
|
+
surfaces: {
|
|
581
|
+
primary: 'grokbot',
|
|
582
|
+
compatibility: [],
|
|
583
|
+
precedence: ['AGENTS.md', 'AIWG_GROKBOT_SKILLS_DIR when configured'],
|
|
584
|
+
related: [],
|
|
585
|
+
},
|
|
586
|
+
// Do not treat xAI API keys or generic GROK_* model env as Grok Bot evidence.
|
|
587
|
+
detection: { env: [], process: [], capabilityId: 'grokbot' },
|
|
588
|
+
paths: {
|
|
589
|
+
deployTarget: 'mixed',
|
|
590
|
+
artifacts: {
|
|
591
|
+
agents: null,
|
|
592
|
+
commands: null,
|
|
593
|
+
// Native skill copies only when AIWG_GROKBOT_SKILLS_DIR is set at deploy time.
|
|
594
|
+
skills: null,
|
|
595
|
+
rules: null,
|
|
596
|
+
behaviors: null,
|
|
597
|
+
},
|
|
598
|
+
kernelSkills: null,
|
|
599
|
+
contextDiscovery: {
|
|
600
|
+
agents: '.agents/agents',
|
|
601
|
+
skills: null,
|
|
602
|
+
rules: null,
|
|
603
|
+
behaviors: null,
|
|
604
|
+
},
|
|
605
|
+
configFile: 'AGENTS.md',
|
|
606
|
+
contextFiles: { aiwgMd: true, agentsMd: true, claudeMdHook: false, hookFile: null, contextFile: 'AGENTS.md' },
|
|
607
|
+
},
|
|
608
|
+
smithPaths: {
|
|
609
|
+
agents: null,
|
|
610
|
+
commands: null,
|
|
611
|
+
skills: null,
|
|
612
|
+
rules: null,
|
|
613
|
+
fileExtension: '.md',
|
|
614
|
+
configFile: 'AGENTS.md',
|
|
615
|
+
aggregated: false,
|
|
616
|
+
},
|
|
617
|
+
// Skills root is env-gated (AIWG_GROKBOT_SKILLS_DIR). skillsBaseDir is a
|
|
618
|
+
// documentation sentinel only — deployer resolves via resolveGrokbotSkillsDir
|
|
619
|
+
// and never joins ~/grokbot-skills.
|
|
620
|
+
skillNamespace: {
|
|
621
|
+
deploymentGroup: 'deep-recursion',
|
|
622
|
+
pathType: 'home-dir',
|
|
623
|
+
skillsBaseDir: 'AIWG_GROKBOT_SKILLS_DIR',
|
|
624
|
+
subdirLayout: true,
|
|
625
|
+
},
|
|
626
|
+
adapters: {
|
|
627
|
+
agentFormat: 'agents-md',
|
|
628
|
+
hookBridge: null,
|
|
629
|
+
mcpInjection: null,
|
|
630
|
+
contextAggregation: 'agents-md',
|
|
631
|
+
ruleFormat: 'agents-md-section',
|
|
632
|
+
},
|
|
633
|
+
matrixRef: 'grokbot',
|
|
634
|
+
},
|
|
559
635
|
{
|
|
560
636
|
id: 'hermes',
|
|
561
637
|
aliases: [],
|
|
@@ -1137,10 +1213,18 @@ export function getProviderArtifactPathStrings(provider) {
|
|
|
1137
1213
|
const definition = getProviderDefinition(provider);
|
|
1138
1214
|
if (!definition)
|
|
1139
1215
|
return undefined;
|
|
1216
|
+
const normalized = normalizeProviderDefinitionId(provider);
|
|
1217
|
+
// Grok Bot skills are env-gated (AIWG_GROKBOT_SKILLS_DIR). Resolve dynamically so
|
|
1218
|
+
// deploy counting / user-scope same-path inventory can see the configured root
|
|
1219
|
+
// without inventing ~/.grokbot (#210).
|
|
1220
|
+
let skills = expandProviderHomePath(definition.paths.artifacts.skills);
|
|
1221
|
+
if (normalized === 'grokbot' && !skills) {
|
|
1222
|
+
skills = resolveGrokbotSkillsDir() ?? '';
|
|
1223
|
+
}
|
|
1140
1224
|
return {
|
|
1141
1225
|
agents: expandProviderHomePath(definition.paths.artifacts.agents),
|
|
1142
1226
|
commands: expandProviderHomePath(definition.paths.artifacts.commands),
|
|
1143
|
-
skills
|
|
1227
|
+
skills,
|
|
1144
1228
|
rules: expandProviderHomePath(definition.paths.artifacts.rules),
|
|
1145
1229
|
behaviors: expandProviderHomePath(definition.paths.artifacts.behaviors),
|
|
1146
1230
|
};
|
|
@@ -12,6 +12,7 @@ const PROVIDER_EXECUTABLES = {
|
|
|
12
12
|
cursor: ['cursor'],
|
|
13
13
|
'deepseek-harness': ['dsh'],
|
|
14
14
|
factory: ['droid'],
|
|
15
|
+
// Grok Bot has no verified CLI executable name; omit rather than invent.
|
|
15
16
|
hermes: ['hermes'],
|
|
16
17
|
opencode: ['opencode'],
|
|
17
18
|
openclaw: ['openclaw'],
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Bot session adapter — manual-export only until product evidence exists
|
|
3
|
+
* for a native session filesystem or API locator.
|
|
4
|
+
*
|
|
5
|
+
* Auto-discover is unsupported and must never scrape invented ~/.grokbot,
|
|
6
|
+
* Cursor session paths, or other unverified roots.
|
|
7
|
+
*
|
|
8
|
+
* Authorized imports reuse the AIWG generic session interchange format so
|
|
9
|
+
* operators can supply an explicit export without inventing a Grok schema.
|
|
10
|
+
*
|
|
11
|
+
* @see docs/providers/grokbot-sessions.md
|
|
12
|
+
* @issue #215
|
|
13
|
+
*/
|
|
14
|
+
import { SessionContractError, } from '../contracts.js';
|
|
15
|
+
import { GenericSessionInterchangeAdapter, } from './generic.js';
|
|
16
|
+
export const GROKBOT_ADAPTER_VERSION = '1.0.0';
|
|
17
|
+
export const GROKBOT_EXPORT_SCHEMA_VERSION = '1.0.0';
|
|
18
|
+
export const GROKBOT_LOCATOR_CLASS = 'manual-export';
|
|
19
|
+
export class GrokbotSessionAdapter {
|
|
20
|
+
provider = 'grokbot';
|
|
21
|
+
adapterVersion = GROKBOT_ADAPTER_VERSION;
|
|
22
|
+
disposition = 'manual-only';
|
|
23
|
+
supportedOperations = ['inspect', 'stream'];
|
|
24
|
+
acquisitionModes = ['manual-export'];
|
|
25
|
+
interchange;
|
|
26
|
+
constructor(limits) {
|
|
27
|
+
this.interchange = new GenericSessionInterchangeAdapter(limits);
|
|
28
|
+
}
|
|
29
|
+
async *discover(_scope) {
|
|
30
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', 'Grok Bot session auto-discover is unsupported until a verified native locator exists; '
|
|
31
|
+
+ 'select an authorized manual export explicitly. AIWG does not scrape ~/.grokbot, '
|
|
32
|
+
+ '~/grokbot-skills, or Cursor session paths.');
|
|
33
|
+
}
|
|
34
|
+
async inspect(source) {
|
|
35
|
+
this.assertManualExport(source);
|
|
36
|
+
return this.interchange.inspect(source);
|
|
37
|
+
}
|
|
38
|
+
async *stream(source, cursor) {
|
|
39
|
+
this.assertManualExport(source);
|
|
40
|
+
yield* this.interchange.stream(source, cursor);
|
|
41
|
+
}
|
|
42
|
+
assertManualExport(source) {
|
|
43
|
+
if (source.locatorClass !== GROKBOT_LOCATOR_CLASS) {
|
|
44
|
+
throw new SessionContractError('UNSUPPORTED_OPERATION', `Grok Bot supports only locatorClass "${GROKBOT_LOCATOR_CLASS}" until native session evidence exists `
|
|
45
|
+
+ `(got "${source.locatorClass}"). Do not reuse Cursor session locators.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=grokbot.js.map
|
|
@@ -4,23 +4,31 @@ import { readBoundedJsonLines, streamBoundedJsonLines, } from '../readers.js';
|
|
|
4
4
|
export const HERMES_ADAPTER_VERSION = '1.0.0';
|
|
5
5
|
export const HERMES_EXPORT_SCHEMA_VERSION = '1.0.0';
|
|
6
6
|
export const HERMES_NATIVE_SCHEMA_VERSION = '23.0.0';
|
|
7
|
+
const sqliteBoolean = z.union([z.boolean(), z.literal(0), z.literal(1)])
|
|
8
|
+
.transform((value) => Boolean(value));
|
|
9
|
+
function nullableOptional(schema) {
|
|
10
|
+
return z.preprocess((value) => value === null ? undefined : value, schema.optional());
|
|
11
|
+
}
|
|
7
12
|
const MessageSchema = z.object({
|
|
8
13
|
id: z.union([z.string(), z.number()]),
|
|
9
14
|
role: z.string().min(1),
|
|
10
15
|
content: z.unknown().optional(),
|
|
11
|
-
tool_call_id: z.string()
|
|
16
|
+
tool_call_id: nullableOptional(z.string()),
|
|
12
17
|
tool_calls: z.unknown().optional(),
|
|
13
|
-
tool_name: z.string()
|
|
18
|
+
tool_name: nullableOptional(z.string()),
|
|
14
19
|
timestamp: z.number().optional(),
|
|
15
|
-
token_count: z.number()
|
|
16
|
-
finish_reason: z.string()
|
|
17
|
-
reasoning: z.string()
|
|
18
|
-
reasoning_content: z.string()
|
|
20
|
+
token_count: nullableOptional(z.number()),
|
|
21
|
+
finish_reason: nullableOptional(z.string()),
|
|
22
|
+
reasoning: nullableOptional(z.string()),
|
|
23
|
+
reasoning_content: nullableOptional(z.string()),
|
|
19
24
|
reasoning_details: z.unknown().optional(),
|
|
20
25
|
codex_reasoning_items: z.unknown().optional(),
|
|
21
26
|
}).passthrough();
|
|
22
27
|
const ExportSchema = z.object({
|
|
23
|
-
|
|
28
|
+
// Hermes CLI exports currently omit a schema marker. They use the same
|
|
29
|
+
// schema-23 record shape that the adapter already validates; explicit
|
|
30
|
+
// markers still pass through the major-version gate below.
|
|
31
|
+
schemaVersion: z.union([z.string(), z.number()]).default(HERMES_NATIVE_SCHEMA_VERSION),
|
|
24
32
|
id: z.string().min(1),
|
|
25
33
|
source: z.string().min(1),
|
|
26
34
|
user_id: z.string().nullable().optional(),
|
|
@@ -31,8 +39,8 @@ const ExportSchema = z.object({
|
|
|
31
39
|
started_at: z.number(),
|
|
32
40
|
ended_at: z.number().nullable().optional(),
|
|
33
41
|
end_reason: z.string().nullable().optional(),
|
|
34
|
-
archived:
|
|
35
|
-
inactive:
|
|
42
|
+
archived: sqliteBoolean.optional(),
|
|
43
|
+
inactive: sqliteBoolean.optional(),
|
|
36
44
|
cwd: z.string().nullable().optional(),
|
|
37
45
|
git_branch: z.string().nullable().optional(),
|
|
38
46
|
routing_key: z.string().nullable().optional(),
|
|
@@ -343,7 +343,18 @@ function normalizeMessage(message, sequence, locatorClass, common) {
|
|
|
343
343
|
toolCallId: part.callID,
|
|
344
344
|
toolState: part.state,
|
|
345
345
|
attachment: part.type === 'file'
|
|
346
|
-
? {
|
|
346
|
+
? {
|
|
347
|
+
mime: part.mime,
|
|
348
|
+
filename: part.filename,
|
|
349
|
+
displayName: part.filename,
|
|
350
|
+
urlPresent: Boolean(part.url),
|
|
351
|
+
// Preserve only locally recoverable byte locators. Remote URLs
|
|
352
|
+
// remain presence-only metadata and are never fetched by export.
|
|
353
|
+
dataBytes: part.url?.startsWith('data:')
|
|
354
|
+
? [...Buffer.from(part.url.slice(part.url.indexOf(',') + 1), part.url.includes(';base64,') ? 'base64' : 'utf8')]
|
|
355
|
+
: undefined,
|
|
356
|
+
fileUrl: part.url?.startsWith('file:') ? part.url : undefined,
|
|
357
|
+
}
|
|
347
358
|
: undefined,
|
|
348
359
|
opaqueContent: !['text', 'reasoning', 'tool', 'file'].includes(part.type),
|
|
349
360
|
unknownFields: unknownFields(part, PART_KEYS),
|
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
export const SESSION_CONTRACT_VERSION = '1.0.0';
|
|
4
4
|
export const SESSION_PROVIDER_IDS = [
|
|
5
5
|
'claude', 'codex', 'copilot', 'cursor', 'factory', 'hermes',
|
|
6
|
-
'opencode', 'openclaw', 'openhuman', 'pi', 'omp', 'deepseek-harness', 'warp', 'devin-desktop', 'generic',
|
|
6
|
+
'opencode', 'openclaw', 'openhuman', 'grokbot', 'pi', 'omp', 'deepseek-harness', 'warp', 'devin-desktop', 'generic',
|
|
7
7
|
];
|
|
8
8
|
export const SessionProviderIdSchema = z.enum(SESSION_PROVIDER_IDS);
|
|
9
9
|
export const SESSION_PROVIDER_ALIASES = Object.freeze({
|
|
@@ -6,17 +6,68 @@
|
|
|
6
6
|
* silently exporting drifted data.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
+
import { lstatSync, readFileSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { resolve, sep } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
9
12
|
import { SessionContractError } from './contracts.js';
|
|
10
13
|
import { buildSessionAiwgFortemiIndexExport, SESSION_EXPORT_INDEX_SCHEMA_VERSION, } from './fortemi-export-mapping.js';
|
|
11
14
|
import { discoverSessionOutputCandidates } from './output-lineage.js';
|
|
12
15
|
import { FilesystemDerivedOutputIndex } from './output-registration.js';
|
|
13
|
-
export const SESSION_EXPORT_PLAN_SCHEMA_VERSION = '1.
|
|
16
|
+
export const SESSION_EXPORT_PLAN_SCHEMA_VERSION = '1.1.0';
|
|
14
17
|
function eventDigestSummary(events) {
|
|
15
18
|
const hash = createHash('sha256');
|
|
16
19
|
for (const event of events)
|
|
17
20
|
hash.update(event.digest);
|
|
18
21
|
return `sha256:${hash.digest('hex')}`;
|
|
19
22
|
}
|
|
23
|
+
function sha256(value) {
|
|
24
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
25
|
+
}
|
|
26
|
+
function sessionMetadataDigest(session, catalogTags) {
|
|
27
|
+
return sha256(JSON.stringify({ session, catalogTags: [...catalogTags].sort() }));
|
|
28
|
+
}
|
|
29
|
+
function localEventAttachment(event, projectRoot) {
|
|
30
|
+
const nativeEnvelope = Object.entries(event.extensions)
|
|
31
|
+
.find(([key, value]) => key.startsWith('native.') && value && typeof value === 'object')?.[1];
|
|
32
|
+
const attachment = event.extensions.attachment ?? nativeEnvelope?.attachment;
|
|
33
|
+
if (!attachment || typeof attachment !== 'object')
|
|
34
|
+
return null;
|
|
35
|
+
const value = attachment;
|
|
36
|
+
const filename = typeof value.displayName === 'string' && value.displayName.length > 0
|
|
37
|
+
? value.displayName
|
|
38
|
+
: typeof value.filename === 'string' && value.filename.length > 0 && !value.filename.startsWith('[REDACTED:')
|
|
39
|
+
? value.filename
|
|
40
|
+
: `${event.eventId}.bin`;
|
|
41
|
+
const mediaType = typeof value.mime === 'string' && value.mime.length > 0 ? value.mime : 'application/octet-stream';
|
|
42
|
+
if (Array.isArray(value.dataBytes)
|
|
43
|
+
&& value.dataBytes.every((item) => Number.isInteger(item) && Number(item) >= 0 && Number(item) <= 255)) {
|
|
44
|
+
return { filename, mediaType, bytes: Uint8Array.from(value.dataBytes) };
|
|
45
|
+
}
|
|
46
|
+
if (typeof value.dataUrl === 'string') {
|
|
47
|
+
const match = /^data:([^;,]*)(;base64)?,(.*)$/s.exec(value.dataUrl);
|
|
48
|
+
if (!match)
|
|
49
|
+
throw new SessionContractError('MALFORMED_SOURCE', `invalid data URL attachment: ${event.eventId}`);
|
|
50
|
+
const bytes = match[2]
|
|
51
|
+
? Buffer.from(match[3], 'base64')
|
|
52
|
+
: Buffer.from(decodeURIComponent(match[3]), 'utf8');
|
|
53
|
+
return { filename, mediaType: match[1] || mediaType, bytes };
|
|
54
|
+
}
|
|
55
|
+
if (typeof value.fileUrl === 'string') {
|
|
56
|
+
if (!projectRoot)
|
|
57
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'project root is required for file attachments');
|
|
58
|
+
const root = realpathSync(projectRoot);
|
|
59
|
+
const candidate = fileURLToPath(value.fileUrl);
|
|
60
|
+
const stat = lstatSync(candidate);
|
|
61
|
+
if (stat.isSymbolicLink())
|
|
62
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `attachment is a symbolic link: ${event.eventId}`);
|
|
63
|
+
const actual = realpathSync(candidate);
|
|
64
|
+
if (actual !== root && !actual.startsWith(`${root}${sep}`)) {
|
|
65
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', `attachment resolves outside the project: ${event.eventId}`);
|
|
66
|
+
}
|
|
67
|
+
return { filename, mediaType, bytes: readFileSync(actual) };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
20
71
|
/**
|
|
21
72
|
* Selects the requested sessions from the repository and builds a plan.
|
|
22
73
|
* Rejects an empty or duplicate selection, and any session id the
|
|
@@ -40,6 +91,7 @@ export function buildSessionExportPlan(repository, options) {
|
|
|
40
91
|
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `session is unknown or not authorized for this workspace: ${sessionId}`);
|
|
41
92
|
}
|
|
42
93
|
const events = repository.listEvents(sessionId, options.workspaceId);
|
|
94
|
+
const catalogTags = repository.listTags(sessionId, options.workspaceId);
|
|
43
95
|
sessions.push(session);
|
|
44
96
|
eventsBySessionId.set(sessionId, events);
|
|
45
97
|
entries.push({
|
|
@@ -48,6 +100,8 @@ export function buildSessionExportPlan(repository, options) {
|
|
|
48
100
|
provider: session.provider,
|
|
49
101
|
sourceId: session.sourceId,
|
|
50
102
|
sourceDigest: session.sourceDigest,
|
|
103
|
+
metadataDigest: sessionMetadataDigest(session, catalogTags),
|
|
104
|
+
catalogTags,
|
|
51
105
|
eventCount: events.length,
|
|
52
106
|
eventDigestSummary: eventDigestSummary(events),
|
|
53
107
|
});
|
|
@@ -61,11 +115,28 @@ export function buildSessionExportPlan(repository, options) {
|
|
|
61
115
|
registrationId: candidate.registration.registrationId,
|
|
62
116
|
outputLocator: candidate.registration.output.locator,
|
|
63
117
|
outputDigest: candidate.registration.output.digest,
|
|
118
|
+
outputByteLength: candidate.registration.output.byteLength,
|
|
64
119
|
mediaType: candidate.registration.output.mediaType,
|
|
65
120
|
lineage: candidate.lineage,
|
|
66
121
|
matchReason: candidate.matchReason,
|
|
67
122
|
}));
|
|
68
|
-
const
|
|
123
|
+
const attachments = [];
|
|
124
|
+
for (const session of sessions) {
|
|
125
|
+
for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
|
|
126
|
+
const attachment = localEventAttachment(event, options.projectRoot);
|
|
127
|
+
if (attachment)
|
|
128
|
+
attachments.push({
|
|
129
|
+
sessionId: session.sessionId,
|
|
130
|
+
eventId: event.eventId,
|
|
131
|
+
filename: attachment.filename,
|
|
132
|
+
mediaType: attachment.mediaType,
|
|
133
|
+
digest: sha256(attachment.bytes),
|
|
134
|
+
byteLength: attachment.bytes.length,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const catalogTagsBySessionId = new Map(entries.map((entry) => [entry.sessionId, entry.catalogTags]));
|
|
139
|
+
const index = buildSessionAiwgFortemiIndexExport(options.workspaceId, sessions, eventsBySessionId, outputs, catalogTagsBySessionId);
|
|
69
140
|
const plan = {
|
|
70
141
|
schemaVersion: SESSION_EXPORT_PLAN_SCHEMA_VERSION,
|
|
71
142
|
indexSchemaVersion: SESSION_EXPORT_INDEX_SCHEMA_VERSION,
|
|
@@ -73,10 +144,12 @@ export function buildSessionExportPlan(repository, options) {
|
|
|
73
144
|
workspaceId: options.workspaceId,
|
|
74
145
|
sessions: entries,
|
|
75
146
|
outputs,
|
|
147
|
+
attachments,
|
|
76
148
|
totals: {
|
|
77
149
|
sessionCount: sessions.length,
|
|
78
150
|
eventCount: entries.reduce((sum, entry) => sum + entry.eventCount, 0),
|
|
79
151
|
outputCount: outputs.length,
|
|
152
|
+
attachmentCount: attachments.length,
|
|
80
153
|
recordCount: index.items.length,
|
|
81
154
|
},
|
|
82
155
|
preview: {
|
|
@@ -94,9 +167,10 @@ export function buildSessionExportPlan(repository, options) {
|
|
|
94
167
|
* acceptance criteria: "Plans reject stale sources ... changed analysis
|
|
95
168
|
* bytes").
|
|
96
169
|
*/
|
|
97
|
-
export function reverifySessionExportPlan(repository, plan) {
|
|
170
|
+
export function reverifySessionExportPlan(repository, plan, options = {}) {
|
|
98
171
|
const sessions = [];
|
|
99
172
|
const eventsBySessionId = new Map();
|
|
173
|
+
const catalogTagsBySessionId = new Map();
|
|
100
174
|
for (const entry of plan.sessions) {
|
|
101
175
|
const session = repository.getSession(entry.sessionId, plan.workspaceId);
|
|
102
176
|
if (!session) {
|
|
@@ -105,13 +179,65 @@ export function reverifySessionExportPlan(repository, plan) {
|
|
|
105
179
|
if (session.sourceDigest !== entry.sourceDigest) {
|
|
106
180
|
throw new SessionContractError('SCHEMA_DRIFT', `session source changed since the plan was generated: ${entry.sessionId}`);
|
|
107
181
|
}
|
|
182
|
+
const catalogTags = repository.listTags(entry.sessionId, plan.workspaceId);
|
|
183
|
+
if (entry.metadataDigest !== sessionMetadataDigest(session, catalogTags)
|
|
184
|
+
|| JSON.stringify(entry.catalogTags) !== JSON.stringify(catalogTags)) {
|
|
185
|
+
throw new SessionContractError('SCHEMA_DRIFT', `session metadata or tags changed since the plan was generated: ${entry.sessionId}`);
|
|
186
|
+
}
|
|
108
187
|
const events = repository.listEvents(entry.sessionId, plan.workspaceId);
|
|
109
188
|
if (events.length !== entry.eventCount || eventDigestSummary(events) !== entry.eventDigestSummary) {
|
|
110
189
|
throw new SessionContractError('SCHEMA_DRIFT', `session events changed since the plan was generated: ${entry.sessionId}`);
|
|
111
190
|
}
|
|
112
191
|
sessions.push(session);
|
|
113
192
|
eventsBySessionId.set(entry.sessionId, events);
|
|
193
|
+
catalogTagsBySessionId.set(entry.sessionId, catalogTags);
|
|
194
|
+
}
|
|
195
|
+
const outputBytesByRegistrationId = new Map();
|
|
196
|
+
if (plan.outputs.length > 0) {
|
|
197
|
+
if (!options.projectRoot) {
|
|
198
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', 'project root is required to verify planned outputs');
|
|
199
|
+
}
|
|
200
|
+
const root = realpathSync(options.projectRoot);
|
|
201
|
+
for (const output of plan.outputs) {
|
|
202
|
+
const candidate = resolve(root, output.outputLocator);
|
|
203
|
+
let stat;
|
|
204
|
+
try {
|
|
205
|
+
stat = lstatSync(candidate);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
throw new SessionContractError('SCHEMA_DRIFT', `registered output is missing since the plan was generated: ${output.outputLocator}`);
|
|
209
|
+
}
|
|
210
|
+
if (stat.isSymbolicLink()) {
|
|
211
|
+
throw new SessionContractError('SOURCE_NOT_AUTHORIZED', `planned output is a symbolic link: ${output.outputLocator}`);
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isFile()) {
|
|
214
|
+
throw new SessionContractError('SCHEMA_DRIFT', `planned output is no longer a regular file: ${output.outputLocator}`);
|
|
215
|
+
}
|
|
216
|
+
if (stat.size !== output.outputByteLength) {
|
|
217
|
+
throw new SessionContractError('SCHEMA_DRIFT', `registered output changed since the plan was generated: ${output.outputLocator}`);
|
|
218
|
+
}
|
|
219
|
+
const actual = realpathSync(candidate);
|
|
220
|
+
if (actual !== root && !actual.startsWith(`${root}${sep}`)) {
|
|
221
|
+
throw new SessionContractError('SOURCE_OUTSIDE_ALLOWED_ROOT', 'planned output resolves outside the project');
|
|
222
|
+
}
|
|
223
|
+
const bytes = readFileSync(actual);
|
|
224
|
+
if (sha256(bytes) !== output.outputDigest) {
|
|
225
|
+
throw new SessionContractError('SCHEMA_DRIFT', `registered output changed since the plan was generated: ${output.outputLocator}`);
|
|
226
|
+
}
|
|
227
|
+
outputBytesByRegistrationId.set(output.registrationId, bytes);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const attachmentBytesByEventId = new Map();
|
|
231
|
+
for (const planned of plan.attachments ?? []) {
|
|
232
|
+
const event = eventsBySessionId.get(planned.sessionId)?.find((candidate) => candidate.eventId === planned.eventId);
|
|
233
|
+
const attachment = event ? localEventAttachment(event, options.projectRoot) : null;
|
|
234
|
+
if (!attachment || attachment.bytes.length !== planned.byteLength || sha256(attachment.bytes) !== planned.digest) {
|
|
235
|
+
throw new SessionContractError('SCHEMA_DRIFT', `session attachment changed since the plan was generated: ${planned.eventId}`);
|
|
236
|
+
}
|
|
237
|
+
attachmentBytesByEventId.set(planned.eventId, attachment.bytes);
|
|
114
238
|
}
|
|
115
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
sessions, eventsBySessionId, catalogTagsBySessionId, outputBytesByRegistrationId, attachmentBytesByEventId,
|
|
241
|
+
};
|
|
116
242
|
}
|
|
117
243
|
//# sourceMappingURL=export-plan.js.map
|
|
@@ -17,7 +17,7 @@ export function sessionRecordLocator(session) {
|
|
|
17
17
|
export function sessionEventRecordLocator(session, event) {
|
|
18
18
|
return `${sessionRecordLocator(session)}/event/${event.eventId}`;
|
|
19
19
|
}
|
|
20
|
-
export function sessionToAiwgFortemiRecord(session) {
|
|
20
|
+
export function sessionToAiwgFortemiRecord(session, catalogTags = []) {
|
|
21
21
|
const locator = sessionRecordLocator(session);
|
|
22
22
|
const updatedAt = session.updatedAt ?? session.startedAt ?? new Date(0).toISOString();
|
|
23
23
|
return {
|
|
@@ -39,7 +39,11 @@ export function sessionToAiwgFortemiRecord(session) {
|
|
|
39
39
|
title: session.intent.title ?? `${session.provider} session ${session.nativeSessionId}`,
|
|
40
40
|
text: session.intent.summary ?? `AIWG session ${session.sessionId} (${session.provider}, ${session.lifecycle})`,
|
|
41
41
|
facets: {},
|
|
42
|
-
tags: [
|
|
42
|
+
tags: [
|
|
43
|
+
tag('provider', session.provider),
|
|
44
|
+
tag('lifecycle', session.lifecycle),
|
|
45
|
+
...catalogTags.map((value) => tag('catalogTag', value)),
|
|
46
|
+
],
|
|
43
47
|
concepts: [],
|
|
44
48
|
relationships: [],
|
|
45
49
|
provenance: [{
|
|
@@ -174,7 +178,7 @@ export function sessionOutputToAiwgFortemiRecord(session, output) {
|
|
|
174
178
|
},
|
|
175
179
|
title: `registered output for ${session.sessionId}`,
|
|
176
180
|
text: `${output.mediaType} output registered at ${output.outputLocator} (${output.matchReason}); `
|
|
177
|
-
+ 'bytes not embedded -- reference and digest only.',
|
|
181
|
+
+ (output.bytesEmbedded ? 'original bytes embedded as a native attachment.' : 'bytes not embedded -- reference and digest only.'),
|
|
178
182
|
facets: {},
|
|
179
183
|
tags: [tag('mediaType', output.mediaType), tag('lineage', 'registered')],
|
|
180
184
|
concepts: [],
|
|
@@ -206,7 +210,7 @@ export function sessionOutputToAiwgFortemiRecord(session, output) {
|
|
|
206
210
|
registrationId: output.registrationId,
|
|
207
211
|
outputLocator: output.outputLocator,
|
|
208
212
|
matchReason: output.matchReason,
|
|
209
|
-
bytesEmbedded:
|
|
213
|
+
bytesEmbedded: output.bytesEmbedded === true,
|
|
210
214
|
},
|
|
211
215
|
}],
|
|
212
216
|
privacy: { classification: 'private', pii: false },
|
|
@@ -271,11 +275,11 @@ function exportManifestRecord(repoRef, generatedAt) {
|
|
|
271
275
|
* session catalog's workspace id, not a git repo -- the field is repurposed
|
|
272
276
|
* honestly rather than left to imply something untrue).
|
|
273
277
|
*/
|
|
274
|
-
export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = []) {
|
|
278
|
+
export function buildSessionAiwgFortemiIndexExport(repoRef, sessions, eventsBySessionId, outputs = [], catalogTagsBySessionId = new Map()) {
|
|
275
279
|
const generatedAt = new Date().toISOString();
|
|
276
280
|
const items = [exportManifestRecord(repoRef, generatedAt)];
|
|
277
281
|
for (const session of sessions) {
|
|
278
|
-
items.push(sessionToAiwgFortemiRecord(session));
|
|
282
|
+
items.push(sessionToAiwgFortemiRecord(session, catalogTagsBySessionId.get(session.sessionId) ?? []));
|
|
279
283
|
for (const event of eventsBySessionId.get(session.sessionId) ?? []) {
|
|
280
284
|
items.push(sessionEventToAiwgFortemiRecord(session, event));
|
|
281
285
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { computeBlobHash, packTarGz, sha256Hex, unpackTarGz, validateFullV1ShardArchive, } from '@fortemi/core';
|
|
2
|
+
function jsonLines(values) {
|
|
3
|
+
return new TextEncoder().encode(`${values.map((value) => JSON.stringify(value)).join('\n')}\n`);
|
|
4
|
+
}
|
|
5
|
+
export async function embedAttachmentsInFullV1Shard(archive, attachments) {
|
|
6
|
+
if (attachments.length === 0)
|
|
7
|
+
return archive;
|
|
8
|
+
const files = unpackTarGz(archive);
|
|
9
|
+
const notesBytes = files.get('notes.jsonl');
|
|
10
|
+
const manifestBytes = files.get('manifest.json');
|
|
11
|
+
if (!notesBytes || !manifestBytes)
|
|
12
|
+
throw new Error('full-v1 shard is missing notes.jsonl or manifest.json');
|
|
13
|
+
const notes = new TextDecoder().decode(notesBytes).split('\n').filter(Boolean)
|
|
14
|
+
.map((line) => JSON.parse(line));
|
|
15
|
+
const byRecordId = new Map(notes.map((note) => [note.metadata?.aiwg_source?.record_id, note]));
|
|
16
|
+
for (const entry of attachments) {
|
|
17
|
+
const note = byRecordId.get(entry.recordId);
|
|
18
|
+
if (!note)
|
|
19
|
+
throw new Error(`cannot attach bytes: shard record is missing: ${entry.recordId}`);
|
|
20
|
+
const checksum = computeBlobHash(entry.bytes);
|
|
21
|
+
note.attachments ??= [];
|
|
22
|
+
note.attachments.push({
|
|
23
|
+
extracted_text: null,
|
|
24
|
+
extraction_status: 'deferred',
|
|
25
|
+
reason: 'no_extracted_text',
|
|
26
|
+
attachment: {
|
|
27
|
+
id: entry.attachmentId,
|
|
28
|
+
path: entry.filename,
|
|
29
|
+
mime: entry.mimeType,
|
|
30
|
+
checksum,
|
|
31
|
+
bytes: entry.bytes.length,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
files.set(`blobs/${checksum.slice('blake3:'.length)}`, entry.bytes);
|
|
35
|
+
}
|
|
36
|
+
const updatedNotes = jsonLines(notes);
|
|
37
|
+
files.set('notes.jsonl', updatedNotes);
|
|
38
|
+
const manifest = JSON.parse(new TextDecoder().decode(manifestBytes));
|
|
39
|
+
manifest.checksums['notes.jsonl'] = await sha256Hex(updatedNotes);
|
|
40
|
+
files.set('manifest.json', new TextEncoder().encode(JSON.stringify(manifest, null, 2)));
|
|
41
|
+
const packed = packTarGz(files);
|
|
42
|
+
const validation = await validateFullV1ShardArchive(packed);
|
|
43
|
+
if (!validation.valid)
|
|
44
|
+
throw new Error(`embedded shard failed validation: ${validation.errors.join('; ')}`);
|
|
45
|
+
return packed;
|
|
46
|
+
}
|
|
47
|
+
export function recoverEmbeddedAttachments(archive) {
|
|
48
|
+
const files = unpackTarGz(archive);
|
|
49
|
+
const notesBytes = files.get('notes.jsonl');
|
|
50
|
+
if (!notesBytes)
|
|
51
|
+
return [];
|
|
52
|
+
const notes = new TextDecoder().decode(notesBytes).split('\n').filter(Boolean)
|
|
53
|
+
.map((line) => JSON.parse(line));
|
|
54
|
+
const recovered = [];
|
|
55
|
+
for (const note of notes) {
|
|
56
|
+
const recordId = note.metadata?.aiwg_source?.record_id;
|
|
57
|
+
if (!recordId)
|
|
58
|
+
continue;
|
|
59
|
+
for (const projection of note.attachments ?? []) {
|
|
60
|
+
const ref = projection.attachment;
|
|
61
|
+
const bytes = files.get(`blobs/${ref.checksum.replace(/^blake3:/, '')}`);
|
|
62
|
+
if (!bytes)
|
|
63
|
+
continue;
|
|
64
|
+
if (bytes.length !== ref.bytes || computeBlobHash(bytes) !== ref.checksum) {
|
|
65
|
+
throw new Error(`embedded attachment failed size/hash verification: ${ref.id}`);
|
|
66
|
+
}
|
|
67
|
+
recovered.push({
|
|
68
|
+
recordId,
|
|
69
|
+
attachmentId: ref.id,
|
|
70
|
+
filename: ref.path,
|
|
71
|
+
mimeType: ref.mime,
|
|
72
|
+
bytes,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return recovered;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=fortemi-shard-blobs.js.map
|
|
@@ -9,6 +9,7 @@ export * from './optional-backends.js';
|
|
|
9
9
|
export * from './knowledge-shard.js';
|
|
10
10
|
export * from './fortemi-export-mapping.js';
|
|
11
11
|
export * from './fortemi-shard-recovery.js';
|
|
12
|
+
export * from './fortemi-shard-blobs.js';
|
|
12
13
|
export * from './output-lineage.js';
|
|
13
14
|
export * from './export-plan.js';
|
|
14
15
|
export * from './candidates.js';
|
|
@@ -32,6 +33,7 @@ export * from './adapters/hermes.js';
|
|
|
32
33
|
export * from './adapters/opencode.js';
|
|
33
34
|
export * from './adapters/openclaw.js';
|
|
34
35
|
export * from './adapters/openhuman.js';
|
|
36
|
+
export * from './adapters/grokbot.js';
|
|
35
37
|
export * from './adapters/pi.js';
|
|
36
38
|
export * from './adapters/deepseek-harness.js';
|
|
37
39
|
export * from './adapters/warp.js';
|
|
@@ -16,7 +16,7 @@ import { redactSourceLocator } from './discovery.js';
|
|
|
16
16
|
import { fingerprintSourceFile } from './readers.js';
|
|
17
17
|
export const DISCOVERY_MANIFEST_VERSION = '1.0.0';
|
|
18
18
|
const MANUAL_EXPORT_PROVIDERS = new Set([
|
|
19
|
-
'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman',
|
|
19
|
+
'copilot', 'hermes', 'opencode', 'openclaw', 'openhuman', 'grokbot',
|
|
20
20
|
'warp', 'devin-desktop', 'generic',
|
|
21
21
|
]);
|
|
22
22
|
export async function discoverWorkspaceHistories(options) {
|