@aiwg/cli 2026.7.21 → 2026.7.23
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/cli/handlers/sessions.js +339 -40
- package/dist/src/config/aiwg-config.js +12 -0
- package/dist/src/config/cli.js +16 -3
- package/dist/src/security/threat-assessment-config.js +296 -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
|
@@ -15,6 +15,7 @@ import { normalizeNamedCaptures } from '../artifacts/index-builder.js';
|
|
|
15
15
|
import { getProviderDefinition, PROVIDER_IDS, resolveProviderPathValue, } from '../providers/provider-definitions.js';
|
|
16
16
|
import { validateAuthorization, } from '../policy/authorization.js';
|
|
17
17
|
import { projectAiwgPath, resolveProjectAiwgDir } from './project-artifacts.js';
|
|
18
|
+
import { defaultThreatAssessmentConfig, validateThreatAssessmentConfig, } from '../security/threat-assessment-config.js';
|
|
18
19
|
const CONFIG_FILENAME = 'aiwg.config';
|
|
19
20
|
/**
|
|
20
21
|
* Operations that a workspace may authorize for one member repository.
|
|
@@ -633,6 +634,9 @@ export function emptyConfig(providers = ['claude']) {
|
|
|
633
634
|
providers,
|
|
634
635
|
installed: {},
|
|
635
636
|
scripts: {},
|
|
637
|
+
security: {
|
|
638
|
+
threatAssessment: defaultThreatAssessmentConfig(),
|
|
639
|
+
},
|
|
636
640
|
delivery: {
|
|
637
641
|
mode: 'pr-required',
|
|
638
642
|
default_branch: 'main',
|
|
@@ -711,12 +715,20 @@ export async function readAiwgConfig(projectDir) {
|
|
|
711
715
|
if (authorizationErrors.length > 0) {
|
|
712
716
|
throw new Error(`Invalid .aiwg/aiwg.config:\n${authorizationErrors.map(item => item.message).join('\n')}`);
|
|
713
717
|
}
|
|
718
|
+
const threatAssessmentErrors = validateThreatAssessmentConfig(parsed.security?.threatAssessment);
|
|
719
|
+
if (threatAssessmentErrors.length > 0) {
|
|
720
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
721
|
+
}
|
|
714
722
|
return parsed;
|
|
715
723
|
}
|
|
716
724
|
/**
|
|
717
725
|
* Write aiwg.config, creating the resolved AIWG artifact directory if needed.
|
|
718
726
|
*/
|
|
719
727
|
export async function writeAiwgConfig(projectDir, config) {
|
|
728
|
+
const threatAssessmentErrors = validateThreatAssessmentConfig(config.security?.threatAssessment);
|
|
729
|
+
if (threatAssessmentErrors.length > 0) {
|
|
730
|
+
throw new Error(`Invalid .aiwg/aiwg.config:\n${threatAssessmentErrors.join('\n')}`);
|
|
731
|
+
}
|
|
720
732
|
const dir = resolveProjectAiwgDir(projectDir);
|
|
721
733
|
await mkdir(dir, { recursive: true });
|
|
722
734
|
const filePath = join(dir, CONFIG_FILENAME);
|
package/dist/src/config/cli.js
CHANGED
|
@@ -152,6 +152,7 @@ const ENUM_RULES = {
|
|
|
152
152
|
'remotes.tracker_actor.via': ['tea', 'gh', 'mcp', 'api'],
|
|
153
153
|
'remotes.transport.protocol': ['ssh', 'https'],
|
|
154
154
|
'repo_maintainer.tiers.local': ['collaborator', 'maintainer', 'admin'],
|
|
155
|
+
'security.threatAssessment.mode': ['off', 'audit', 'enforce'],
|
|
155
156
|
};
|
|
156
157
|
const BOOLEAN_FIELDS = new Set([
|
|
157
158
|
'delivery.delete_branch_on_merge',
|
|
@@ -214,15 +215,17 @@ async function projectConfigSet(key, raw, args) {
|
|
|
214
215
|
}
|
|
215
216
|
// Coerce booleans for known boolean fields
|
|
216
217
|
let value = raw;
|
|
217
|
-
if (/^externalLinks\.[^.]+$/.test(key)) {
|
|
218
|
+
if (/^externalLinks\.[^.]+$/.test(key) || key === 'security.threatAssessment') {
|
|
218
219
|
try {
|
|
219
220
|
value = JSON.parse(raw);
|
|
220
221
|
}
|
|
221
222
|
catch {
|
|
222
223
|
throw new AiwgError({
|
|
223
224
|
code: 'ERR_INVALID_VALUE',
|
|
224
|
-
message: `${key} must be a JSON object
|
|
225
|
-
hint:
|
|
225
|
+
message: `${key} must be a valid JSON object`,
|
|
226
|
+
hint: key === 'security.threatAssessment'
|
|
227
|
+
? `Try: aiwg config set --project ${key} '{"schemaVersion":"1","mode":"audit","defaultProfile":"balanced"}'`
|
|
228
|
+
: `Try: aiwg config set --project ${key} '{"label":"Project docs","url":"https://example.com/docs"}'`,
|
|
226
229
|
exitCode: EXIT_CODES.USAGE,
|
|
227
230
|
});
|
|
228
231
|
}
|
|
@@ -285,6 +288,16 @@ async function projectConfigSet(key, raw, args) {
|
|
|
285
288
|
exitCode: EXIT_CODES.USAGE,
|
|
286
289
|
});
|
|
287
290
|
}
|
|
291
|
+
const { validateThreatAssessmentConfig } = await import('../security/threat-assessment-config.js');
|
|
292
|
+
const threatErrors = validateThreatAssessmentConfig(cfg.security?.threatAssessment);
|
|
293
|
+
if (threatErrors.length > 0) {
|
|
294
|
+
throw new AiwgError({
|
|
295
|
+
code: 'ERR_INVALID_VALUE',
|
|
296
|
+
message: `Invalid threat-assessment configuration: ${threatErrors.join('; ')}`,
|
|
297
|
+
hint: 'Use a built-in profile or correct the referenced profile, rule pack, threshold, or regex.',
|
|
298
|
+
exitCode: EXIT_CODES.USAGE,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
288
301
|
await writeAiwgConfig(projectDir, cfg);
|
|
289
302
|
console.log(`Set --project ${key} = ${raw}`);
|
|
290
303
|
}
|
|
@@ -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
|
|
@@ -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
|
},
|