@aiwg/cli 2026.7.21 → 2026.7.24
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 +14 -3
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +2 -0
- package/dist/src/artifacts/index-builder.js +44 -8
- package/dist/src/artifacts/query-engine.js +1 -1
- package/dist/src/artifacts/types.js +1 -0
- package/dist/src/cli/handlers/index.js +5 -1
- package/dist/src/cli/handlers/sessions.js +339 -40
- package/dist/src/cli/handlers/setup-manifest.js +800 -0
- package/dist/src/cli/handlers/use.js +127 -17
- package/dist/src/config/aiwg-config.js +18 -2
- package/dist/src/config/cli.js +16 -3
- package/dist/src/extensions/commands/definitions.js +99 -0
- package/dist/src/security/threat-assessment-config.js +296 -0
- package/dist/src/serve/sandbox-registry.js +34 -0
- package/dist/src/sessions/adapters/claude.js +37 -9
- package/dist/src/sessions/adapters/codex.js +38 -11
- package/dist/src/sessions/adapters/cursor.js +166 -10
- package/dist/src/sessions/adapters/factory.js +50 -9
- package/dist/src/sessions/batch-contracts.js +121 -0
- package/dist/src/sessions/batch-import.js +265 -0
- package/dist/src/sessions/contracts.js +32 -5
- package/dist/src/sessions/import-lease.js +152 -0
- package/dist/src/sessions/importer.js +163 -14
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/origin.js +117 -0
- package/dist/src/sessions/readers.js +1 -1
- package/dist/src/sessions/repository.js +354 -13
- package/dist/src/sessions/timeline.js +148 -0
- package/dist/src/sessions/workspace-discovery.js +319 -0
- package/package.json +1 -1
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
export const THREAT_ASSESSMENT_MODES = ['off', 'audit', 'enforce'];
|
|
2
|
+
export const THREAT_ASSESSMENT_SEVERITIES = [
|
|
3
|
+
'informational',
|
|
4
|
+
'low',
|
|
5
|
+
'moderate',
|
|
6
|
+
'high',
|
|
7
|
+
'critical',
|
|
8
|
+
];
|
|
9
|
+
export const THREAT_ASSESSMENT_SURFACES = [
|
|
10
|
+
'issue-title',
|
|
11
|
+
'issue-body',
|
|
12
|
+
'issue-comment',
|
|
13
|
+
'pull-request-title',
|
|
14
|
+
'pull-request-body',
|
|
15
|
+
'pull-request-diff-summary',
|
|
16
|
+
'review-comment',
|
|
17
|
+
'release-note',
|
|
18
|
+
'handoff',
|
|
19
|
+
'outbound-maintainer-comment',
|
|
20
|
+
];
|
|
21
|
+
const BUILTIN_PROFILES = new Set(['trusted', 'audit', 'balanced', 'strict', 'high-assurance']);
|
|
22
|
+
const BUILTIN_RULE_PACKS = new Set([
|
|
23
|
+
'aiwg:all',
|
|
24
|
+
'aiwg:prompt-injection',
|
|
25
|
+
'aiwg:supply-chain',
|
|
26
|
+
'aiwg:credential-protection',
|
|
27
|
+
]);
|
|
28
|
+
const BUILTIN_RULE_IDS = new Set([
|
|
29
|
+
'instruction-override',
|
|
30
|
+
'sensitive-file-target',
|
|
31
|
+
'third-party-execution',
|
|
32
|
+
'floating-version',
|
|
33
|
+
'credential-or-env-probing',
|
|
34
|
+
'pressure-without-evidence',
|
|
35
|
+
'unverifiable-authority-claim',
|
|
36
|
+
'security-framing-conflict',
|
|
37
|
+
]);
|
|
38
|
+
function objectValue(value) {
|
|
39
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function stringArray(value) {
|
|
42
|
+
return Array.isArray(value) && value.every(item => typeof item === 'string');
|
|
43
|
+
}
|
|
44
|
+
function validateStatement(statement, where, errors) {
|
|
45
|
+
if (!objectValue(statement)) {
|
|
46
|
+
errors.push(`${where}: must be an object`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (typeof statement.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(statement.id)) {
|
|
50
|
+
errors.push(`${where}.id: must be a kebab-case identifier`);
|
|
51
|
+
}
|
|
52
|
+
if (!['suppress', 'set-severity'].includes(String(statement.effect))) {
|
|
53
|
+
errors.push(`${where}.effect: must be suppress or set-severity`);
|
|
54
|
+
}
|
|
55
|
+
if (typeof statement.reason !== 'string' || !statement.reason.trim()) {
|
|
56
|
+
errors.push(`${where}.reason: must be a non-empty string`);
|
|
57
|
+
}
|
|
58
|
+
if (statement.signals !== undefined && !stringArray(statement.signals)) {
|
|
59
|
+
errors.push(`${where}.signals: must be an array of strings`);
|
|
60
|
+
}
|
|
61
|
+
if (statement.severity !== undefined
|
|
62
|
+
&& !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
|
|
63
|
+
errors.push(`${where}.severity: unknown severity`);
|
|
64
|
+
}
|
|
65
|
+
if (statement.when !== undefined && !objectValue(statement.when)) {
|
|
66
|
+
errors.push(`${where}.when: must be an object`);
|
|
67
|
+
}
|
|
68
|
+
if (statement.riskAcceptance !== undefined) {
|
|
69
|
+
if (!objectValue(statement.riskAcceptance)
|
|
70
|
+
|| typeof statement.riskAcceptance.acceptedBy !== 'string'
|
|
71
|
+
|| typeof statement.riskAcceptance.rationale !== 'string') {
|
|
72
|
+
errors.push(`${where}.riskAcceptance: requires acceptedBy and rationale`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (statement.effect === 'suppress') {
|
|
76
|
+
if (!stringArray(statement.signals) || statement.signals.length === 0) {
|
|
77
|
+
errors.push(`${where}.signals: suppress statements must name at least one signal`);
|
|
78
|
+
}
|
|
79
|
+
const when = objectValue(statement.when) ? statement.when : undefined;
|
|
80
|
+
if (!when || !Object.values(when).some(value => Array.isArray(value) && value.length > 0)) {
|
|
81
|
+
errors.push(`${where}.when: suppress statements require at least one narrow condition`);
|
|
82
|
+
}
|
|
83
|
+
if (!objectValue(statement.riskAcceptance)
|
|
84
|
+
|| typeof statement.riskAcceptance.acceptedBy !== 'string'
|
|
85
|
+
|| typeof statement.riskAcceptance.rationale !== 'string') {
|
|
86
|
+
errors.push(`${where}.riskAcceptance: suppress statements require acceptedBy and rationale`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (statement.effect === 'set-severity'
|
|
90
|
+
&& !THREAT_ASSESSMENT_SEVERITIES.includes(statement.severity)) {
|
|
91
|
+
errors.push(`${where}.severity: set-severity requires a known severity`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fail-closed validation for project threat-assessment policy. The evaluator
|
|
96
|
+
* performs the same checks at runtime; config loading validates first so a
|
|
97
|
+
* malformed policy never degrades into the default or off mode.
|
|
98
|
+
*/
|
|
99
|
+
export function validateThreatAssessmentConfig(value) {
|
|
100
|
+
const errors = [];
|
|
101
|
+
if (value === undefined || value === null)
|
|
102
|
+
return errors;
|
|
103
|
+
if (!objectValue(value))
|
|
104
|
+
return ['security.threatAssessment: must be an object'];
|
|
105
|
+
if (value.schemaVersion !== undefined && value.schemaVersion !== '1') {
|
|
106
|
+
errors.push("security.threatAssessment.schemaVersion: must be '1'");
|
|
107
|
+
}
|
|
108
|
+
if (value.mode !== undefined
|
|
109
|
+
&& !THREAT_ASSESSMENT_MODES.includes(value.mode)) {
|
|
110
|
+
errors.push('security.threatAssessment.mode: must be off, audit, or enforce');
|
|
111
|
+
}
|
|
112
|
+
if (value.defaultProfile !== undefined && typeof value.defaultProfile !== 'string') {
|
|
113
|
+
errors.push('security.threatAssessment.defaultProfile: must be a string');
|
|
114
|
+
}
|
|
115
|
+
if (value.surfaces !== undefined) {
|
|
116
|
+
if (!objectValue(value.surfaces))
|
|
117
|
+
errors.push('security.threatAssessment.surfaces: must be an object');
|
|
118
|
+
else {
|
|
119
|
+
for (const [surface, entry] of Object.entries(value.surfaces)) {
|
|
120
|
+
const where = `security.threatAssessment.surfaces.${surface}`;
|
|
121
|
+
if (!THREAT_ASSESSMENT_SURFACES.includes(surface)) {
|
|
122
|
+
errors.push(`${where}: unknown surface`);
|
|
123
|
+
}
|
|
124
|
+
if (!objectValue(entry)) {
|
|
125
|
+
errors.push(`${where}: must be an object`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (entry.mode !== undefined
|
|
129
|
+
&& !THREAT_ASSESSMENT_MODES.includes(entry.mode)) {
|
|
130
|
+
errors.push(`${where}.mode: must be off, audit, or enforce`);
|
|
131
|
+
}
|
|
132
|
+
if (entry.profile !== undefined && typeof entry.profile !== 'string') {
|
|
133
|
+
errors.push(`${where}.profile: must be a string`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const rulePacks = objectValue(value.rulePacks) ? value.rulePacks : {};
|
|
139
|
+
if (value.rulePacks !== undefined && !objectValue(value.rulePacks)) {
|
|
140
|
+
errors.push('security.threatAssessment.rulePacks: must be an object');
|
|
141
|
+
}
|
|
142
|
+
for (const [name, pack] of Object.entries(rulePacks)) {
|
|
143
|
+
const where = `security.threatAssessment.rulePacks.${name}`;
|
|
144
|
+
if (name.startsWith('aiwg:'))
|
|
145
|
+
errors.push(`${where}: cannot shadow a built-in rule pack`);
|
|
146
|
+
if (!objectValue(pack) || !Array.isArray(pack.rules)) {
|
|
147
|
+
errors.push(`${where}.rules: must be an array`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (typeof pack.version !== 'string' || !pack.version.trim()) {
|
|
151
|
+
errors.push(`${where}.version: must be a non-empty string`);
|
|
152
|
+
}
|
|
153
|
+
const ids = new Set();
|
|
154
|
+
pack.rules.forEach((rule, index) => {
|
|
155
|
+
const ruleWhere = `${where}.rules[${index}]`;
|
|
156
|
+
if (!objectValue(rule)) {
|
|
157
|
+
errors.push(`${ruleWhere}: must be an object`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (typeof rule.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(rule.id)) {
|
|
161
|
+
errors.push(`${ruleWhere}.id: must be a kebab-case identifier`);
|
|
162
|
+
}
|
|
163
|
+
else if (BUILTIN_RULE_IDS.has(rule.id)) {
|
|
164
|
+
errors.push(`${ruleWhere}.id: cannot shadow built-in rule '${rule.id}'`);
|
|
165
|
+
}
|
|
166
|
+
else if (ids.has(rule.id)) {
|
|
167
|
+
errors.push(`${ruleWhere}.id: duplicate '${rule.id}'`);
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
ids.add(rule.id);
|
|
171
|
+
if (!THREAT_ASSESSMENT_SEVERITIES.includes(rule.severity)) {
|
|
172
|
+
errors.push(`${ruleWhere}.severity: unknown severity`);
|
|
173
|
+
}
|
|
174
|
+
for (const dimension of ['likelihood', 'impact']) {
|
|
175
|
+
const dimensionValue = rule[dimension];
|
|
176
|
+
if (dimensionValue !== undefined
|
|
177
|
+
&& (!Number.isInteger(dimensionValue) || Number(dimensionValue) < 1 || Number(dimensionValue) > 5)) {
|
|
178
|
+
errors.push(`${ruleWhere}.${dimension}: must be an integer from 1 to 5`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!stringArray(rule.patterns) || rule.patterns.length === 0) {
|
|
182
|
+
errors.push(`${ruleWhere}.patterns: must be a non-empty array of strings`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
rule.patterns.forEach((pattern, patternIndex) => {
|
|
186
|
+
const patternWhere = `${ruleWhere}.patterns[${patternIndex}]`;
|
|
187
|
+
if (!pattern || pattern.length > 500) {
|
|
188
|
+
errors.push(`${patternWhere}: must be 1-500 characters`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
new RegExp(pattern, 'imu');
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
errors.push(`${patternWhere}: invalid regular expression (${error.message})`);
|
|
196
|
+
}
|
|
197
|
+
if (/\\[1-9]/.test(pattern) || /\(\?<([=!])/.test(pattern)
|
|
198
|
+
|| /\([^)]*[+*][^)]*\)[+*{]/.test(pattern) || /(?:\.\*){2,}/.test(pattern)) {
|
|
199
|
+
errors.push(`${patternWhere}: unsafe regex construct (backreference, lookbehind, or nested unbounded quantifier)`);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const profiles = objectValue(value.profiles) ? value.profiles : {};
|
|
206
|
+
if (value.profiles !== undefined && !objectValue(value.profiles)) {
|
|
207
|
+
errors.push('security.threatAssessment.profiles: must be an object');
|
|
208
|
+
}
|
|
209
|
+
const visit = (name, stack) => {
|
|
210
|
+
if (stack.includes(name)) {
|
|
211
|
+
errors.push(`security.threatAssessment.profiles.${name}.extends: cyclic inheritance (${[...stack, name].join(' -> ')})`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const profile = profiles[name];
|
|
215
|
+
if (!objectValue(profile))
|
|
216
|
+
return;
|
|
217
|
+
const parents = profile.extends ?? [];
|
|
218
|
+
if (!stringArray(parents)) {
|
|
219
|
+
errors.push(`security.threatAssessment.profiles.${name}.extends: must be an array of strings`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const rawParent of parents) {
|
|
223
|
+
const parent = rawParent.replace(/^aiwg:/, '');
|
|
224
|
+
if (!BUILTIN_PROFILES.has(parent) && !(parent in profiles)) {
|
|
225
|
+
errors.push(`security.threatAssessment.profiles.${name}.extends: unknown profile '${rawParent}'`);
|
|
226
|
+
}
|
|
227
|
+
else if (parent in profiles)
|
|
228
|
+
visit(parent, [...stack, name]);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
232
|
+
const where = `security.threatAssessment.profiles.${name}`;
|
|
233
|
+
if (BUILTIN_PROFILES.has(name) || name.startsWith('aiwg:')) {
|
|
234
|
+
errors.push(`${where}: cannot shadow a built-in profile`);
|
|
235
|
+
}
|
|
236
|
+
if (!objectValue(profile)) {
|
|
237
|
+
errors.push(`${where}: must be an object`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (profile.mode !== undefined
|
|
241
|
+
&& !THREAT_ASSESSMENT_MODES.includes(profile.mode)) {
|
|
242
|
+
errors.push(`${where}.mode: must be off, audit, or enforce`);
|
|
243
|
+
}
|
|
244
|
+
if (profile.version !== undefined && (typeof profile.version !== 'string' || !profile.version.trim())) {
|
|
245
|
+
errors.push(`${where}.version: must be a non-empty string`);
|
|
246
|
+
}
|
|
247
|
+
if (profile.ruleSets !== undefined && !stringArray(profile.ruleSets)) {
|
|
248
|
+
errors.push(`${where}.ruleSets: must be an array of strings`);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
for (const pack of (profile.ruleSets ?? [])) {
|
|
252
|
+
if (!BUILTIN_RULE_PACKS.has(pack) && !(pack in rulePacks)) {
|
|
253
|
+
errors.push(`${where}.ruleSets: unavailable rule pack '${pack}'`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (profile.thresholds !== undefined) {
|
|
258
|
+
if (!objectValue(profile.thresholds))
|
|
259
|
+
errors.push(`${where}.thresholds: must be an object`);
|
|
260
|
+
else {
|
|
261
|
+
for (const [action, severity] of Object.entries(profile.thresholds)) {
|
|
262
|
+
if (!['flag', 'requireAuthorization', 'reject'].includes(action)
|
|
263
|
+
|| !THREAT_ASSESSMENT_SEVERITIES.includes(severity)) {
|
|
264
|
+
errors.push(`${where}.thresholds.${action}: unknown threshold or severity`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (profile.statements !== undefined) {
|
|
270
|
+
if (!Array.isArray(profile.statements))
|
|
271
|
+
errors.push(`${where}.statements: must be an array`);
|
|
272
|
+
else
|
|
273
|
+
profile.statements.forEach((statement, index) => validateStatement(statement, `${where}.statements[${index}]`, errors));
|
|
274
|
+
}
|
|
275
|
+
visit(name, []);
|
|
276
|
+
}
|
|
277
|
+
if (value.statements !== undefined) {
|
|
278
|
+
if (!Array.isArray(value.statements))
|
|
279
|
+
errors.push('security.threatAssessment.statements: must be an array');
|
|
280
|
+
else
|
|
281
|
+
value.statements.forEach((statement, index) => validateStatement(statement, `security.threatAssessment.statements[${index}]`, errors));
|
|
282
|
+
}
|
|
283
|
+
const defaultProfile = typeof value.defaultProfile === 'string' ? value.defaultProfile : 'balanced';
|
|
284
|
+
if (!BUILTIN_PROFILES.has(defaultProfile) && !(defaultProfile in profiles)) {
|
|
285
|
+
errors.push(`security.threatAssessment.defaultProfile: unknown profile '${defaultProfile}'`);
|
|
286
|
+
}
|
|
287
|
+
return Array.from(new Set(errors));
|
|
288
|
+
}
|
|
289
|
+
export function defaultThreatAssessmentConfig() {
|
|
290
|
+
return {
|
|
291
|
+
schemaVersion: '1',
|
|
292
|
+
mode: 'enforce',
|
|
293
|
+
defaultProfile: 'balanced',
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=threat-assessment-config.js.map
|
|
@@ -241,6 +241,35 @@ export function normalizeSandboxEvent(raw) {
|
|
|
241
241
|
* Matches the sandbox's 5 s retry interval — suppressess flicker on rapid restarts.
|
|
242
242
|
*/
|
|
243
243
|
const DEBOUNCE_MS = 5_000;
|
|
244
|
+
function safeTrustRef(value) {
|
|
245
|
+
const ref = typeof value === 'string' ? value.trim() : '';
|
|
246
|
+
if (!ref)
|
|
247
|
+
return undefined;
|
|
248
|
+
if (/-----BEGIN|PRIVATE KEY|TOKEN|SECRET|PASSWORD|[\r\n]/i.test(ref))
|
|
249
|
+
return '[redacted]';
|
|
250
|
+
return ref.slice(0, 160);
|
|
251
|
+
}
|
|
252
|
+
function sanitizeTrustPosture(posture) {
|
|
253
|
+
if (!posture || typeof posture !== 'object')
|
|
254
|
+
return undefined;
|
|
255
|
+
const status = ['secure', 'degraded', 'disabled', 'unknown'].includes(String(posture.status))
|
|
256
|
+
? posture.status
|
|
257
|
+
: 'unknown';
|
|
258
|
+
return {
|
|
259
|
+
status,
|
|
260
|
+
mode: safeTrustRef(posture.mode),
|
|
261
|
+
ca_provider_ref: safeTrustRef(posture.ca_provider_ref),
|
|
262
|
+
trust_bundle_ref: safeTrustRef(posture.trust_bundle_ref),
|
|
263
|
+
client_identity_ref: safeTrustRef(posture.client_identity_ref),
|
|
264
|
+
rotation_state: safeTrustRef(posture.rotation_state),
|
|
265
|
+
expires_at: safeTrustRef(posture.expires_at),
|
|
266
|
+
trust_bundle_fresh: posture.trust_bundle_fresh,
|
|
267
|
+
missing_required_material: Array.isArray(posture.missing_required_material)
|
|
268
|
+
? posture.missing_required_material.map((item) => safeTrustRef(item)).filter((item) => Boolean(item))
|
|
269
|
+
: undefined,
|
|
270
|
+
recovery: safeTrustRef(posture.recovery),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
244
273
|
export class SandboxRegistry {
|
|
245
274
|
sandboxes = new Map();
|
|
246
275
|
hitlRequests = new Map();
|
|
@@ -336,6 +365,9 @@ export class SandboxRegistry {
|
|
|
336
365
|
if (req.ws_capabilities) {
|
|
337
366
|
existing.wsCapabilities = req.ws_capabilities;
|
|
338
367
|
}
|
|
368
|
+
if (req.trust_posture) {
|
|
369
|
+
existing.trustPosture = sanitizeTrustPosture(req.trust_posture);
|
|
370
|
+
}
|
|
339
371
|
this.lastRegistrationTime.set(instanceId, now);
|
|
340
372
|
return { sandbox_id: existingId, token: existing.token };
|
|
341
373
|
}
|
|
@@ -370,6 +402,7 @@ export class SandboxRegistry {
|
|
|
370
402
|
agents: new Map(),
|
|
371
403
|
sandboxInventory,
|
|
372
404
|
wsCapabilities: req.ws_capabilities,
|
|
405
|
+
trustPosture: sanitizeTrustPosture(req.trust_posture),
|
|
373
406
|
};
|
|
374
407
|
this.sandboxes.set(id, registration);
|
|
375
408
|
if (instanceId) {
|
|
@@ -825,6 +858,7 @@ function toSummary(s) {
|
|
|
825
858
|
agents: [...s.agents.values()],
|
|
826
859
|
sandboxInventory: s.sandboxInventory,
|
|
827
860
|
wsCapabilities: s.wsCapabilities,
|
|
861
|
+
trustPosture: s.trustPosture,
|
|
828
862
|
};
|
|
829
863
|
}
|
|
830
864
|
// Singleton instance
|
|
@@ -16,6 +16,10 @@ const ClaudeRecordSchema = z.object({
|
|
|
16
16
|
timestamp: z.string().datetime({ offset: true }).optional(),
|
|
17
17
|
cwd: z.string().optional(),
|
|
18
18
|
gitBranch: z.string().optional(),
|
|
19
|
+
parentSessionId: z.string().min(1).optional(),
|
|
20
|
+
isSidechain: z.boolean().optional(),
|
|
21
|
+
agentId: z.string().min(1).optional(),
|
|
22
|
+
slug: z.string().min(1).optional(),
|
|
19
23
|
version: z.string().optional(),
|
|
20
24
|
schemaVersion: z.string().optional(),
|
|
21
25
|
message: z.object({
|
|
@@ -138,13 +142,11 @@ function normalizeTranscript(records, locator) {
|
|
|
138
142
|
throw new SessionContractError('MALFORMED_SOURCE', 'Claude transcript record is malformed');
|
|
139
143
|
}
|
|
140
144
|
const value = parsed.data;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
throw new SessionContractError('SCHEMA_DRIFT', 'Claude transcript session identity differs from its documented filename identity');
|
|
144
|
-
}
|
|
145
|
+
const embeddedSessionId = value.sessionId ?? value.session_id;
|
|
146
|
+
const nativeSessionId = embeddedSessionId ?? filenameSessionId;
|
|
145
147
|
const blocks = messageBlocks(value);
|
|
146
148
|
if (blocks.length === 0) {
|
|
147
|
-
output.push(providerRecord(value, record, nativeSessionId, {
|
|
149
|
+
output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
|
|
148
150
|
kind: `claude.${value.type}`,
|
|
149
151
|
text: '',
|
|
150
152
|
opaque: true,
|
|
@@ -153,7 +155,7 @@ function normalizeTranscript(records, locator) {
|
|
|
153
155
|
continue;
|
|
154
156
|
}
|
|
155
157
|
for (const [blockIndex, block] of blocks.entries()) {
|
|
156
|
-
output.push(providerRecord(value, record, nativeSessionId, {
|
|
158
|
+
output.push(providerRecord(value, record, nativeSessionId, filenameSessionId, {
|
|
157
159
|
kind: block.kind,
|
|
158
160
|
text: block.text,
|
|
159
161
|
blockIndex,
|
|
@@ -184,11 +186,24 @@ function normalizeHooks(records) {
|
|
|
184
186
|
participant: 'claude',
|
|
185
187
|
model: hook.model,
|
|
186
188
|
occurredAt: hook.timestamp,
|
|
189
|
+
activityBoundary: hook.hook_event_name === 'SessionEnd'
|
|
190
|
+
? 'end'
|
|
191
|
+
: hook.source === 'resume'
|
|
192
|
+
? 'resume'
|
|
193
|
+
: hook.source === 'compact'
|
|
194
|
+
? 'continuation'
|
|
195
|
+
: undefined,
|
|
196
|
+
activityBoundaryBasis: hook.hook_event_name === 'SessionEnd' || hook.source
|
|
197
|
+
? `claude-hook:${hook.hook_event_name}:${hook.source ?? 'none'}`
|
|
198
|
+
: undefined,
|
|
199
|
+
activityBoundaryConfidence: hook.hook_event_name === 'SessionEnd' || hook.source
|
|
200
|
+
? 'high'
|
|
201
|
+
: undefined,
|
|
187
202
|
text: '',
|
|
188
203
|
rawReference: { locatorClass: 'claude-hook-jsonl', offset: record.byteOffset },
|
|
189
204
|
extensions: {
|
|
190
205
|
hookEventName: hook.hook_event_name,
|
|
191
|
-
|
|
206
|
+
...(hook.hook_event_name === 'SessionEnd' ? { lifecycle: 'complete' } : {}),
|
|
192
207
|
startSource: hook.source,
|
|
193
208
|
model: hook.model,
|
|
194
209
|
reason: hook.reason,
|
|
@@ -201,7 +216,7 @@ function normalizeHooks(records) {
|
|
|
201
216
|
}
|
|
202
217
|
return { records: output, complete };
|
|
203
218
|
}
|
|
204
|
-
function providerRecord(value, record, nativeSessionId, block) {
|
|
219
|
+
function providerRecord(value, record, nativeSessionId, sourceArtifactSessionId, block) {
|
|
205
220
|
const nativeBase = value.uuid ?? value.message?.id;
|
|
206
221
|
const nativeEventId = block.blockNativeId
|
|
207
222
|
?? (nativeBase ? `${nativeBase}:${block.blockIndex ?? 0}` : undefined);
|
|
@@ -221,6 +236,18 @@ function providerRecord(value, record, nativeSessionId, block) {
|
|
|
221
236
|
productVersion: value.version,
|
|
222
237
|
workspace: { cwdClass: value.cwd ? '<workspace>' : undefined, gitBranch: value.gitBranch },
|
|
223
238
|
provenance: { acquisition: 'claude-transcript', schema: CLAUDE_TRANSCRIPT_SCHEMA_VERSION },
|
|
239
|
+
transcriptFamily: {
|
|
240
|
+
sourceArtifactSessionId,
|
|
241
|
+
nativeSessionId,
|
|
242
|
+
identityRelation: nativeSessionId === sourceArtifactSessionId ? 'self' : 'related',
|
|
243
|
+
parentUuid: value.parentUuid,
|
|
244
|
+
parentSessionId: value.parentSessionId,
|
|
245
|
+
gitBranch: value.gitBranch,
|
|
246
|
+
continuation: record.sequence > 0 && value.parentUuid !== null,
|
|
247
|
+
subagent: value.isSidechain === true || Boolean(value.agentId),
|
|
248
|
+
agentId: value.agentId,
|
|
249
|
+
agentSlug: value.slug,
|
|
250
|
+
},
|
|
224
251
|
opaque: block.opaque,
|
|
225
252
|
unknownFields: {
|
|
226
253
|
...unknownFields(value, TRANSCRIPT_KEYS),
|
|
@@ -348,7 +375,8 @@ function unknownFields(value, known) {
|
|
|
348
375
|
}
|
|
349
376
|
const TRANSCRIPT_KEYS = new Set([
|
|
350
377
|
'type', 'subtype', 'uuid', 'parentUuid', 'sessionId', 'session_id',
|
|
351
|
-
'timestamp', 'cwd', 'gitBranch', '
|
|
378
|
+
'timestamp', 'cwd', 'gitBranch', 'parentSessionId', 'isSidechain',
|
|
379
|
+
'agentId', 'slug', 'version', 'schemaVersion', 'message',
|
|
352
380
|
]);
|
|
353
381
|
const HOOK_KEYS = new Set([
|
|
354
382
|
'session_id', 'transcript_path', 'cwd', 'hook_event_name', 'permission_mode',
|
|
@@ -83,7 +83,8 @@ export class CodexSessionAdapter {
|
|
|
83
83
|
let mode = isAppServerLocator(source.locator)
|
|
84
84
|
? 'app-server' : null;
|
|
85
85
|
let schemaVersion = null;
|
|
86
|
-
|
|
86
|
+
const sourceArtifactSessionId = rolloutIdFromFilename(source.locator);
|
|
87
|
+
let nativeSessionId = sourceArtifactSessionId;
|
|
87
88
|
const identities = new Map();
|
|
88
89
|
let outputIndex = 0;
|
|
89
90
|
let sawRecord = false;
|
|
@@ -118,15 +119,12 @@ export class CodexSessionAdapter {
|
|
|
118
119
|
if (!declaredId) {
|
|
119
120
|
throw new SessionContractError('MALFORMED_SOURCE', 'Codex session metadata is missing its id');
|
|
120
121
|
}
|
|
121
|
-
if (nativeSessionId && nativeSessionId !== declaredId) {
|
|
122
|
-
throw new SessionContractError('SCHEMA_DRIFT', 'Codex rollout session identity differs from its filename identity');
|
|
123
|
-
}
|
|
124
122
|
nativeSessionId = declaredId;
|
|
125
123
|
}
|
|
126
124
|
if (!nativeSessionId) {
|
|
127
125
|
throw new SessionContractError('MALFORMED_SOURCE', 'Codex rollout has no session identity before content records');
|
|
128
126
|
}
|
|
129
|
-
normalized = [rolloutRecord(nativeSessionId, parsed.data, payload, line)];
|
|
127
|
+
normalized = [rolloutRecord(nativeSessionId, parsed.data, payload, line, sourceArtifactSessionId)];
|
|
130
128
|
}
|
|
131
129
|
for (const record of normalized) {
|
|
132
130
|
if (outputIndex++ >= start)
|
|
@@ -188,6 +186,15 @@ function normalizeAppServer(input) {
|
|
|
188
186
|
sequence: line.sequence * 1_000,
|
|
189
187
|
kind: event === 'compacted' ? 'summary' : 'codex.lifecycle',
|
|
190
188
|
role: 'system',
|
|
189
|
+
activityBoundary: event === 'unarchived'
|
|
190
|
+
? 'resume'
|
|
191
|
+
: event === 'compacted'
|
|
192
|
+
? 'continuation'
|
|
193
|
+
: event === 'archived' || event === 'deleted'
|
|
194
|
+
? 'end'
|
|
195
|
+
: undefined,
|
|
196
|
+
activityBoundaryBasis: `codex-app-server:${event}`,
|
|
197
|
+
activityBoundaryConfidence: event === 'status-changed' ? 'medium' : 'high',
|
|
191
198
|
text: '',
|
|
192
199
|
rawReference: { locatorClass: 'codex-app-server-jsonl', offset: line.byteOffset },
|
|
193
200
|
extensions: {
|
|
@@ -201,7 +208,8 @@ function normalizeAppServer(input) {
|
|
|
201
208
|
return { records, complete: complete && !sawActive };
|
|
202
209
|
}
|
|
203
210
|
function normalizeRollout(input, locator) {
|
|
204
|
-
|
|
211
|
+
const sourceArtifactSessionId = rolloutIdFromFilename(locator);
|
|
212
|
+
let nativeSessionId = sourceArtifactSessionId;
|
|
205
213
|
const records = [];
|
|
206
214
|
for (const line of input) {
|
|
207
215
|
const parsed = RolloutEnvelopeSchema.safeParse(line.value);
|
|
@@ -215,15 +223,12 @@ function normalizeRollout(input, locator) {
|
|
|
215
223
|
if (!declaredId) {
|
|
216
224
|
throw new SessionContractError('MALFORMED_SOURCE', 'Codex session metadata is missing its id');
|
|
217
225
|
}
|
|
218
|
-
if (nativeSessionId && nativeSessionId !== declaredId) {
|
|
219
|
-
throw new SessionContractError('SCHEMA_DRIFT', 'Codex rollout session identity differs from its filename identity');
|
|
220
|
-
}
|
|
221
226
|
nativeSessionId = declaredId;
|
|
222
227
|
}
|
|
223
228
|
if (!nativeSessionId) {
|
|
224
229
|
throw new SessionContractError('MALFORMED_SOURCE', 'Codex rollout has no session identity before content records');
|
|
225
230
|
}
|
|
226
|
-
records.push(rolloutRecord(nativeSessionId, envelope, payload, line));
|
|
231
|
+
records.push(rolloutRecord(nativeSessionId, envelope, payload, line, sourceArtifactSessionId));
|
|
227
232
|
}
|
|
228
233
|
return { records, complete: false };
|
|
229
234
|
}
|
|
@@ -281,7 +286,7 @@ function threadItem(thread, turn, item, line, turnIndex, itemIndex) {
|
|
|
281
286
|
},
|
|
282
287
|
};
|
|
283
288
|
}
|
|
284
|
-
function rolloutRecord(nativeSessionId, envelope, payload, line) {
|
|
289
|
+
function rolloutRecord(nativeSessionId, envelope, payload, line, sourceArtifactSessionId) {
|
|
285
290
|
const nativeId = stringValue(payload.id)
|
|
286
291
|
?? stringValue(payload.call_id)
|
|
287
292
|
?? `${envelope.type}:${line.sequence}`;
|
|
@@ -298,6 +303,21 @@ function rolloutRecord(nativeSessionId, envelope, payload, line) {
|
|
|
298
303
|
toolCallId: stringValue(payload.call_id),
|
|
299
304
|
model: stringValue(payload.model),
|
|
300
305
|
occurredAt: envelope.timestamp,
|
|
306
|
+
activityBoundary: envelope.type === 'compacted'
|
|
307
|
+
? 'continuation'
|
|
308
|
+
: envelope.type === 'event_msg'
|
|
309
|
+
&& stringValue(payload.type) === 'task_complete'
|
|
310
|
+
? 'end'
|
|
311
|
+
: undefined,
|
|
312
|
+
activityBoundaryBasis: envelope.type === 'compacted'
|
|
313
|
+
? 'codex-rollout:compacted'
|
|
314
|
+
: envelope.type === 'event_msg' && stringValue(payload.type) === 'task_complete'
|
|
315
|
+
? 'codex-rollout:task-complete'
|
|
316
|
+
: undefined,
|
|
317
|
+
activityBoundaryConfidence: envelope.type === 'compacted'
|
|
318
|
+
|| (envelope.type === 'event_msg' && stringValue(payload.type) === 'task_complete')
|
|
319
|
+
? 'high'
|
|
320
|
+
: undefined,
|
|
301
321
|
text: rolloutText(payload),
|
|
302
322
|
rawReference: { locatorClass: 'codex-rollout-jsonl', offset: line.byteOffset },
|
|
303
323
|
extensions: {
|
|
@@ -310,6 +330,13 @@ function rolloutRecord(nativeSessionId, envelope, payload, line) {
|
|
|
310
330
|
: undefined,
|
|
311
331
|
productVersion: stringValue(payload.cli_version),
|
|
312
332
|
provenance: { acquisition: 'codex-rollout', durableReplay: true },
|
|
333
|
+
transcriptFamily: {
|
|
334
|
+
sourceArtifactSessionId,
|
|
335
|
+
nativeSessionId,
|
|
336
|
+
identityRelation: !sourceArtifactSessionId || sourceArtifactSessionId === nativeSessionId
|
|
337
|
+
? 'self'
|
|
338
|
+
: 'related',
|
|
339
|
+
},
|
|
313
340
|
opaque: !KNOWN_ROLLOUT_TYPES.has(envelope.type),
|
|
314
341
|
unknownFields: unknownFields(payload, ROLLOUT_KEYS),
|
|
315
342
|
},
|