trustdex 0.5.1 → 0.7.0
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.ko.md +65 -7
- package/README.md +71 -7
- package/bin/trustdex.mjs +98 -1
- package/docs/ARCHITECTURE.ko.md +9 -2
- package/docs/ARCHITECTURE.md +11 -2
- package/docs/CLAUDE.ko.md +54 -1
- package/docs/CLAUDE.md +69 -1
- package/docs/CODEX.ko.md +50 -4
- package/docs/CODEX.md +62 -4
- package/docs/DEFENSIVE_USE_CASE.ko.md +85 -0
- package/docs/DEFENSIVE_USE_CASE.md +89 -0
- package/docs/EVALUATION.ko.md +46 -1
- package/docs/EVALUATION.md +48 -1
- package/docs/ROADMAP.ko.md +17 -2
- package/docs/ROADMAP.md +17 -2
- package/docs/SECURITY_RESEARCH.ko.md +7 -0
- package/docs/SECURITY_RESEARCH.md +7 -0
- package/docs/THREAT_MODEL.ko.md +8 -1
- package/docs/THREAT_MODEL.md +8 -1
- package/examples/adversarial/README.ko.md +31 -0
- package/examples/adversarial/README.md +31 -0
- package/examples/adversarial/drift-after.json +11 -0
- package/examples/adversarial/drift-before.json +8 -0
- package/examples/adversarial/expected.json +30 -0
- package/examples/adversarial/mcp-config.json +32 -0
- package/examples/adversarial/permissions/README.ko.md +15 -0
- package/examples/adversarial/permissions/README.md +15 -0
- package/examples/adversarial/permissions/claude-expanded.settings.json +30 -0
- package/examples/adversarial/permissions/claude-restrictive.settings.json +16 -0
- package/examples/adversarial/permissions/codex-expanded.toml +13 -0
- package/examples/adversarial/permissions/codex-restrictive.toml +13 -0
- package/examples/adversarial/permissions/expected.json +18 -0
- package/examples/adversarial/policy.json +10 -0
- package/examples/claude.mcp.json +24 -0
- package/examples/claude.policy.json +6 -0
- package/examples/claude.settings.json +26 -0
- package/examples/codex.config.toml +16 -0
- package/examples/codex.permissions.toml +13 -0
- package/examples/mcp.json +22 -0
- package/examples/trust-store.example.json +16 -0
- package/examples/trustdex.policy.json +14 -0
- package/package.json +5 -3
- package/schemas/permission-posture-diff-v1.schema.json +78 -0
- package/schemas/permission-posture-v1.schema.json +162 -0
- package/src/claude-settings.mjs +173 -0
- package/src/codex-config.mjs +9 -7
- package/src/codex-settings.mjs +247 -0
- package/src/policy-packs.mjs +38 -2
- package/src/posture.mjs +540 -0
package/src/policy-packs.mjs
CHANGED
|
@@ -14,7 +14,39 @@ const BASE_SIGNALS = {
|
|
|
14
14
|
'embedded-credentials': 'block',
|
|
15
15
|
'insecure-transport': 'block',
|
|
16
16
|
'url-query-parameters': 'ask',
|
|
17
|
-
'artifact-version-mismatch': 'block'
|
|
17
|
+
'artifact-version-mismatch': 'block',
|
|
18
|
+
'claude-auto-approve': 'ask',
|
|
19
|
+
'claude-broad-auto-approve': 'block',
|
|
20
|
+
'claude-shell-auto-approve': 'block',
|
|
21
|
+
'claude-filesystem-auto-approve': 'ask',
|
|
22
|
+
'claude-network-auto-approve': 'ask',
|
|
23
|
+
'claude-subagent-auto-approve': 'ask',
|
|
24
|
+
'claude-mcp-auto-approve': 'ask',
|
|
25
|
+
'claude-mcp-wildcard': 'ask',
|
|
26
|
+
'claude-bypass-permissions': 'block',
|
|
27
|
+
'claude-auto-mode': 'ask',
|
|
28
|
+
'claude-additional-directories': 'ask',
|
|
29
|
+
'claude-outside-read-access': 'ask',
|
|
30
|
+
'claude-hooks': 'block',
|
|
31
|
+
'claude-credential-helper': 'block',
|
|
32
|
+
'claude-environment': 'ask',
|
|
33
|
+
'claude-sandbox-settings': 'ask',
|
|
34
|
+
'claude-extension-loading': 'ask',
|
|
35
|
+
'uninspected-claude-settings': 'ask',
|
|
36
|
+
'codex-no-approvals': 'ask',
|
|
37
|
+
'codex-auto-review': 'ask',
|
|
38
|
+
'codex-workspace-write': 'ask',
|
|
39
|
+
'codex-full-access': 'block',
|
|
40
|
+
'codex-network-access': 'ask',
|
|
41
|
+
'codex-additional-writable-roots': 'ask',
|
|
42
|
+
'codex-temp-write-access': 'ask',
|
|
43
|
+
'codex-live-web-search': 'ask',
|
|
44
|
+
'codex-custom-permissions': 'ask',
|
|
45
|
+
'codex-subagents-enabled': 'ask',
|
|
46
|
+
'codex-subagent-settings': 'ask',
|
|
47
|
+
'codex-extension-settings': 'ask',
|
|
48
|
+
'uninspected-codex-settings': 'ask',
|
|
49
|
+
'uninspected-codex-sections': 'ask'
|
|
18
50
|
};
|
|
19
51
|
|
|
20
52
|
export const POLICY_PACKS = Object.freeze({
|
|
@@ -59,7 +91,11 @@ export const POLICY_PACKS = Object.freeze({
|
|
|
59
91
|
signalActions: {
|
|
60
92
|
...BASE_SIGNALS,
|
|
61
93
|
'shell-execution': 'ask',
|
|
62
|
-
'insecure-transport': 'ask'
|
|
94
|
+
'insecure-transport': 'ask',
|
|
95
|
+
'claude-broad-auto-approve': 'ask',
|
|
96
|
+
'claude-shell-auto-approve': 'ask',
|
|
97
|
+
'claude-hooks': 'ask',
|
|
98
|
+
'claude-credential-helper': 'ask'
|
|
63
99
|
}
|
|
64
100
|
})
|
|
65
101
|
});
|
package/src/posture.mjs
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { sha256Json } from './canonical.mjs';
|
|
2
|
+
|
|
3
|
+
const ACTION_RANK = Object.freeze({ allow: 0, ask: 1, block: 2 });
|
|
4
|
+
const SNAPSHOT_KIND = 'agent-permission-posture';
|
|
5
|
+
const SNAPSHOT_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
const CLAUDE_MODES = new Set([
|
|
8
|
+
null,
|
|
9
|
+
'default',
|
|
10
|
+
'manual',
|
|
11
|
+
'acceptEdits',
|
|
12
|
+
'plan',
|
|
13
|
+
'dontAsk',
|
|
14
|
+
'bypassPermissions',
|
|
15
|
+
'auto'
|
|
16
|
+
]);
|
|
17
|
+
const CLAUDE_CATEGORIES = new Set(['filesystem', 'mcp', 'network', 'other', 'shell', 'subagent']);
|
|
18
|
+
const CODEX_APPROVALS = new Set([null, 'on-request', 'never', 'granular']);
|
|
19
|
+
const CODEX_REVIEWERS = new Set([null, 'user', 'auto_review']);
|
|
20
|
+
const CODEX_SANDBOXES = new Set([null, 'read-only', 'workspace-write', 'danger-full-access']);
|
|
21
|
+
const CODEX_PERMISSIONS = new Set([null, ':read-only', ':workspace', ':danger-full-access', 'custom']);
|
|
22
|
+
const CODEX_SEARCH = new Set([null, 'disabled', 'cached', 'indexed', 'live']);
|
|
23
|
+
const CODEX_PROMPTS = new Set([
|
|
24
|
+
'mcp_elicitations',
|
|
25
|
+
'request_permissions',
|
|
26
|
+
'rules',
|
|
27
|
+
'sandbox_approval',
|
|
28
|
+
'skill_approval'
|
|
29
|
+
]);
|
|
30
|
+
const RUNTIME_SIGNALS = Object.freeze({
|
|
31
|
+
claude: new Set([
|
|
32
|
+
'claude-auto-approve',
|
|
33
|
+
'claude-broad-auto-approve',
|
|
34
|
+
'claude-shell-auto-approve',
|
|
35
|
+
'claude-filesystem-auto-approve',
|
|
36
|
+
'claude-network-auto-approve',
|
|
37
|
+
'claude-subagent-auto-approve',
|
|
38
|
+
'claude-mcp-auto-approve',
|
|
39
|
+
'claude-mcp-wildcard',
|
|
40
|
+
'claude-bypass-permissions',
|
|
41
|
+
'claude-auto-mode',
|
|
42
|
+
'claude-additional-directories',
|
|
43
|
+
'claude-outside-read-access',
|
|
44
|
+
'claude-hooks',
|
|
45
|
+
'claude-credential-helper',
|
|
46
|
+
'claude-environment',
|
|
47
|
+
'claude-sandbox-settings',
|
|
48
|
+
'claude-extension-loading',
|
|
49
|
+
'uninspected-claude-settings',
|
|
50
|
+
'secret-env'
|
|
51
|
+
]),
|
|
52
|
+
codex: new Set([
|
|
53
|
+
'codex-no-approvals',
|
|
54
|
+
'codex-auto-review',
|
|
55
|
+
'codex-workspace-write',
|
|
56
|
+
'codex-full-access',
|
|
57
|
+
'codex-network-access',
|
|
58
|
+
'codex-additional-writable-roots',
|
|
59
|
+
'codex-temp-write-access',
|
|
60
|
+
'codex-live-web-search',
|
|
61
|
+
'codex-custom-permissions',
|
|
62
|
+
'codex-subagents-enabled',
|
|
63
|
+
'codex-subagent-settings',
|
|
64
|
+
'codex-extension-settings',
|
|
65
|
+
'uninspected-codex-settings',
|
|
66
|
+
'uninspected-codex-sections'
|
|
67
|
+
])
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function requireObject(value, message) {
|
|
71
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(message);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function requireCount(value, field) {
|
|
76
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`Invalid permission posture count: ${field}`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function requireBoolean(value, field) {
|
|
81
|
+
if (typeof value !== 'boolean') throw new Error(`Invalid permission posture boolean: ${field}`);
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function requireEnum(value, allowed, field) {
|
|
86
|
+
if (!allowed.has(value)) throw new Error(`Invalid permission posture value: ${field}`);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function requireStringArray(value, allowed, field) {
|
|
91
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || !allowed.has(item))) {
|
|
92
|
+
throw new Error(`Invalid permission posture list: ${field}`);
|
|
93
|
+
}
|
|
94
|
+
return [...new Set(value)].sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function assertMetadataKeys(metadata, expected) {
|
|
98
|
+
for (const key of Object.keys(metadata)) {
|
|
99
|
+
if (!expected.has(key)) throw new Error(`Unsupported permission posture metadata: ${key}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeClaudeMetadata(metadata) {
|
|
104
|
+
const expected = new Set([
|
|
105
|
+
'defaultMode',
|
|
106
|
+
'allowRuleCount',
|
|
107
|
+
'askRuleCount',
|
|
108
|
+
'denyRuleCount',
|
|
109
|
+
'allowCategories',
|
|
110
|
+
'additionalDirectoryCount',
|
|
111
|
+
'bypassDisabled',
|
|
112
|
+
'autoModeDisabled',
|
|
113
|
+
'outsideReadsBlocked',
|
|
114
|
+
'uninspectedRootKeys'
|
|
115
|
+
]);
|
|
116
|
+
assertMetadataKeys(metadata, expected);
|
|
117
|
+
if (!Array.isArray(metadata.uninspectedRootKeys) ||
|
|
118
|
+
metadata.uninspectedRootKeys.some((item) => typeof item !== 'string')) {
|
|
119
|
+
throw new Error('Invalid Claude uninspected root key metadata.');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
defaultMode: requireEnum(metadata.defaultMode, CLAUDE_MODES, 'defaultMode'),
|
|
123
|
+
allowRuleCount: requireCount(metadata.allowRuleCount, 'allowRuleCount'),
|
|
124
|
+
askRuleCount: requireCount(metadata.askRuleCount, 'askRuleCount'),
|
|
125
|
+
denyRuleCount: requireCount(metadata.denyRuleCount, 'denyRuleCount'),
|
|
126
|
+
allowCategories: requireStringArray(metadata.allowCategories, CLAUDE_CATEGORIES, 'allowCategories'),
|
|
127
|
+
additionalDirectoryCount: requireCount(metadata.additionalDirectoryCount, 'additionalDirectoryCount'),
|
|
128
|
+
bypassDisabled: requireBoolean(metadata.bypassDisabled, 'bypassDisabled'),
|
|
129
|
+
autoModeDisabled: requireBoolean(metadata.autoModeDisabled, 'autoModeDisabled'),
|
|
130
|
+
outsideReadsBlocked: requireBoolean(metadata.outsideReadsBlocked, 'outsideReadsBlocked'),
|
|
131
|
+
uninspectedRootKeyCount: metadata.uninspectedRootKeys.length
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function normalizeCodexMetadata(metadata) {
|
|
136
|
+
const expected = new Set([
|
|
137
|
+
'approvalPolicy',
|
|
138
|
+
'granularPromptCategories',
|
|
139
|
+
'approvalsReviewer',
|
|
140
|
+
'sandboxMode',
|
|
141
|
+
'defaultPermissionsKind',
|
|
142
|
+
'webSearch',
|
|
143
|
+
'networkAccess',
|
|
144
|
+
'additionalWritableRootCount',
|
|
145
|
+
'tempRootsExcluded',
|
|
146
|
+
'subagentsEnabled',
|
|
147
|
+
'uninspectedAgentKeys',
|
|
148
|
+
'uninspectedRootKeys',
|
|
149
|
+
'uninspectedSectionKinds'
|
|
150
|
+
]);
|
|
151
|
+
assertMetadataKeys(metadata, expected);
|
|
152
|
+
for (const field of ['uninspectedAgentKeys', 'uninspectedRootKeys', 'uninspectedSectionKinds']) {
|
|
153
|
+
if (!Array.isArray(metadata[field]) || metadata[field].some((item) => typeof item !== 'string')) {
|
|
154
|
+
throw new Error(`Invalid Codex ${field} metadata.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
approvalPolicy: requireEnum(metadata.approvalPolicy, CODEX_APPROVALS, 'approvalPolicy'),
|
|
159
|
+
granularPromptCategories: requireStringArray(
|
|
160
|
+
metadata.granularPromptCategories,
|
|
161
|
+
CODEX_PROMPTS,
|
|
162
|
+
'granularPromptCategories'
|
|
163
|
+
),
|
|
164
|
+
approvalsReviewer: requireEnum(metadata.approvalsReviewer, CODEX_REVIEWERS, 'approvalsReviewer'),
|
|
165
|
+
sandboxMode: requireEnum(metadata.sandboxMode, CODEX_SANDBOXES, 'sandboxMode'),
|
|
166
|
+
defaultPermissionsKind: requireEnum(
|
|
167
|
+
metadata.defaultPermissionsKind,
|
|
168
|
+
CODEX_PERMISSIONS,
|
|
169
|
+
'defaultPermissionsKind'
|
|
170
|
+
),
|
|
171
|
+
webSearch: requireEnum(metadata.webSearch, CODEX_SEARCH, 'webSearch'),
|
|
172
|
+
networkAccess: requireBoolean(metadata.networkAccess, 'networkAccess'),
|
|
173
|
+
additionalWritableRootCount: requireCount(
|
|
174
|
+
metadata.additionalWritableRootCount,
|
|
175
|
+
'additionalWritableRootCount'
|
|
176
|
+
),
|
|
177
|
+
tempRootsExcluded: requireBoolean(metadata.tempRootsExcluded, 'tempRootsExcluded'),
|
|
178
|
+
subagentsEnabled: requireBoolean(metadata.subagentsEnabled, 'subagentsEnabled'),
|
|
179
|
+
uninspectedAgentKeyCount: metadata.uninspectedAgentKeys.length,
|
|
180
|
+
uninspectedRootKeyCount: metadata.uninspectedRootKeys.length,
|
|
181
|
+
uninspectedSectionKindCount: metadata.uninspectedSectionKinds.length
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function projectAction(value) {
|
|
186
|
+
return Object.hasOwn(ACTION_RANK, value) ? value : value == null ? null : 'invalid';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function projectPolicy(policy, runtime) {
|
|
190
|
+
const relevantSignals = RUNTIME_SIGNALS[runtime];
|
|
191
|
+
const signalActions = Object.fromEntries(
|
|
192
|
+
Object.entries(policy?.signalActions || {})
|
|
193
|
+
.filter(([signal]) => relevantSignals.has(signal))
|
|
194
|
+
.map(([signal, action]) => [signal, projectAction(action)])
|
|
195
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
196
|
+
);
|
|
197
|
+
return {
|
|
198
|
+
unknownAction: projectAction(policy?.unknownAction),
|
|
199
|
+
localAction: projectAction(policy?.localAction),
|
|
200
|
+
sensitiveAction: projectAction(policy?.sensitiveAction),
|
|
201
|
+
signalActions
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function snapshotDigest(snapshot) {
|
|
206
|
+
return sha256Json({
|
|
207
|
+
schemaVersion: snapshot.schemaVersion,
|
|
208
|
+
kind: snapshot.kind,
|
|
209
|
+
runtime: snapshot.runtime,
|
|
210
|
+
policyDigest: snapshot.policyDigest,
|
|
211
|
+
posture: snapshot.posture
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function createPermissionPostureSnapshot(result, policy = {}) {
|
|
216
|
+
requireObject(result, 'Permission posture requires one evaluated settings result.');
|
|
217
|
+
if (!Object.hasOwn(ACTION_RANK, result.action)) throw new Error('Permission posture result has an invalid action.');
|
|
218
|
+
if (!Array.isArray(result.signals) || result.signals.some((item) => typeof item !== 'string')) {
|
|
219
|
+
throw new Error('Permission posture result has invalid signals.');
|
|
220
|
+
}
|
|
221
|
+
const runtime = result.kind === 'claude-settings'
|
|
222
|
+
? 'claude'
|
|
223
|
+
: result.kind === 'codex-settings' ? 'codex' : null;
|
|
224
|
+
if (!runtime) throw new Error('Permission posture supports Claude or Codex settings results only.');
|
|
225
|
+
if (result.signals.some((signal) => !RUNTIME_SIGNALS[runtime].has(signal))) {
|
|
226
|
+
throw new Error('Permission posture result contains an unsupported signal.');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const metadata = requireObject(result.metadata, 'Permission posture result is missing metadata.');
|
|
230
|
+
const posture = {
|
|
231
|
+
action: result.action,
|
|
232
|
+
signals: [...new Set(result.signals)].sort(),
|
|
233
|
+
metadata: runtime === 'claude'
|
|
234
|
+
? normalizeClaudeMetadata(metadata)
|
|
235
|
+
: normalizeCodexMetadata(metadata)
|
|
236
|
+
};
|
|
237
|
+
const snapshot = {
|
|
238
|
+
schemaVersion: SNAPSHOT_VERSION,
|
|
239
|
+
kind: SNAPSHOT_KIND,
|
|
240
|
+
createdAt: new Date().toISOString(),
|
|
241
|
+
runtime,
|
|
242
|
+
policyDigest: sha256Json(projectPolicy(policy, runtime)),
|
|
243
|
+
posture
|
|
244
|
+
};
|
|
245
|
+
return { ...snapshot, digest: snapshotDigest(snapshot) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function validateSnapshot(snapshot) {
|
|
249
|
+
requireObject(snapshot, 'Invalid permission posture snapshot.');
|
|
250
|
+
if (snapshot.schemaVersion !== SNAPSHOT_VERSION || snapshot.kind !== SNAPSHOT_KIND) {
|
|
251
|
+
throw new Error('Unsupported permission posture snapshot schema.');
|
|
252
|
+
}
|
|
253
|
+
if (!['claude', 'codex'].includes(snapshot.runtime)) throw new Error('Invalid permission posture runtime.');
|
|
254
|
+
if (typeof snapshot.policyDigest !== 'string' || !/^[a-f0-9]{64}$/.test(snapshot.policyDigest)) {
|
|
255
|
+
throw new Error('Invalid permission posture policy digest.');
|
|
256
|
+
}
|
|
257
|
+
requireObject(snapshot.posture, 'Permission posture snapshot is missing posture data.');
|
|
258
|
+
if (!Object.hasOwn(ACTION_RANK, snapshot.posture.action)) throw new Error('Invalid permission posture action.');
|
|
259
|
+
if (!Array.isArray(snapshot.posture.signals) ||
|
|
260
|
+
snapshot.posture.signals.some((item) => typeof item !== 'string')) {
|
|
261
|
+
throw new Error('Invalid permission posture signals.');
|
|
262
|
+
}
|
|
263
|
+
const normalizedSignals = [...new Set(snapshot.posture.signals)].sort();
|
|
264
|
+
if (sha256Json(normalizedSignals) !== sha256Json(snapshot.posture.signals)) {
|
|
265
|
+
throw new Error('Non-canonical permission posture signals.');
|
|
266
|
+
}
|
|
267
|
+
const metadata = requireObject(snapshot.posture.metadata, 'Permission posture snapshot is missing metadata.');
|
|
268
|
+
if (snapshot.runtime === 'claude') {
|
|
269
|
+
const normalized = normalizeClaudeSnapshotMetadata(metadata);
|
|
270
|
+
if (sha256Json(normalized) !== sha256Json(metadata)) throw new Error('Non-canonical Claude posture metadata.');
|
|
271
|
+
} else {
|
|
272
|
+
const normalized = normalizeCodexSnapshotMetadata(metadata);
|
|
273
|
+
if (sha256Json(normalized) !== sha256Json(metadata)) throw new Error('Non-canonical Codex posture metadata.');
|
|
274
|
+
}
|
|
275
|
+
if (snapshot.digest !== snapshotDigest(snapshot)) throw new Error('Permission posture snapshot digest mismatch.');
|
|
276
|
+
return snapshot;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeClaudeSnapshotMetadata(metadata) {
|
|
280
|
+
const expected = new Set([
|
|
281
|
+
'defaultMode', 'allowRuleCount', 'askRuleCount', 'denyRuleCount', 'allowCategories',
|
|
282
|
+
'additionalDirectoryCount', 'bypassDisabled', 'autoModeDisabled', 'outsideReadsBlocked',
|
|
283
|
+
'uninspectedRootKeyCount'
|
|
284
|
+
]);
|
|
285
|
+
assertMetadataKeys(metadata, expected);
|
|
286
|
+
return {
|
|
287
|
+
defaultMode: requireEnum(metadata.defaultMode, CLAUDE_MODES, 'defaultMode'),
|
|
288
|
+
allowRuleCount: requireCount(metadata.allowRuleCount, 'allowRuleCount'),
|
|
289
|
+
askRuleCount: requireCount(metadata.askRuleCount, 'askRuleCount'),
|
|
290
|
+
denyRuleCount: requireCount(metadata.denyRuleCount, 'denyRuleCount'),
|
|
291
|
+
allowCategories: requireStringArray(metadata.allowCategories, CLAUDE_CATEGORIES, 'allowCategories'),
|
|
292
|
+
additionalDirectoryCount: requireCount(metadata.additionalDirectoryCount, 'additionalDirectoryCount'),
|
|
293
|
+
bypassDisabled: requireBoolean(metadata.bypassDisabled, 'bypassDisabled'),
|
|
294
|
+
autoModeDisabled: requireBoolean(metadata.autoModeDisabled, 'autoModeDisabled'),
|
|
295
|
+
outsideReadsBlocked: requireBoolean(metadata.outsideReadsBlocked, 'outsideReadsBlocked'),
|
|
296
|
+
uninspectedRootKeyCount: requireCount(metadata.uninspectedRootKeyCount, 'uninspectedRootKeyCount')
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function normalizeCodexSnapshotMetadata(metadata) {
|
|
301
|
+
const expected = new Set([
|
|
302
|
+
'approvalPolicy', 'granularPromptCategories', 'approvalsReviewer', 'sandboxMode',
|
|
303
|
+
'defaultPermissionsKind', 'webSearch', 'networkAccess', 'additionalWritableRootCount',
|
|
304
|
+
'tempRootsExcluded', 'subagentsEnabled', 'uninspectedAgentKeyCount',
|
|
305
|
+
'uninspectedRootKeyCount', 'uninspectedSectionKindCount'
|
|
306
|
+
]);
|
|
307
|
+
assertMetadataKeys(metadata, expected);
|
|
308
|
+
return {
|
|
309
|
+
approvalPolicy: requireEnum(metadata.approvalPolicy, CODEX_APPROVALS, 'approvalPolicy'),
|
|
310
|
+
granularPromptCategories: requireStringArray(
|
|
311
|
+
metadata.granularPromptCategories,
|
|
312
|
+
CODEX_PROMPTS,
|
|
313
|
+
'granularPromptCategories'
|
|
314
|
+
),
|
|
315
|
+
approvalsReviewer: requireEnum(metadata.approvalsReviewer, CODEX_REVIEWERS, 'approvalsReviewer'),
|
|
316
|
+
sandboxMode: requireEnum(metadata.sandboxMode, CODEX_SANDBOXES, 'sandboxMode'),
|
|
317
|
+
defaultPermissionsKind: requireEnum(
|
|
318
|
+
metadata.defaultPermissionsKind,
|
|
319
|
+
CODEX_PERMISSIONS,
|
|
320
|
+
'defaultPermissionsKind'
|
|
321
|
+
),
|
|
322
|
+
webSearch: requireEnum(metadata.webSearch, CODEX_SEARCH, 'webSearch'),
|
|
323
|
+
networkAccess: requireBoolean(metadata.networkAccess, 'networkAccess'),
|
|
324
|
+
additionalWritableRootCount: requireCount(
|
|
325
|
+
metadata.additionalWritableRootCount,
|
|
326
|
+
'additionalWritableRootCount'
|
|
327
|
+
),
|
|
328
|
+
tempRootsExcluded: requireBoolean(metadata.tempRootsExcluded, 'tempRootsExcluded'),
|
|
329
|
+
subagentsEnabled: requireBoolean(metadata.subagentsEnabled, 'subagentsEnabled'),
|
|
330
|
+
uninspectedAgentKeyCount: requireCount(metadata.uninspectedAgentKeyCount, 'uninspectedAgentKeyCount'),
|
|
331
|
+
uninspectedRootKeyCount: requireCount(metadata.uninspectedRootKeyCount, 'uninspectedRootKeyCount'),
|
|
332
|
+
uninspectedSectionKindCount: requireCount(
|
|
333
|
+
metadata.uninspectedSectionKindCount,
|
|
334
|
+
'uninspectedSectionKindCount'
|
|
335
|
+
)
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function addChange(changes, impact, type, field, before, after) {
|
|
340
|
+
changes.push({ impact, type, field, before, after });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function compareCount(changes, field, before, after, riskIncreasing = true) {
|
|
344
|
+
if (before === after) return;
|
|
345
|
+
const increased = after > before;
|
|
346
|
+
const impact = increased === riskIncreasing ? 'expansion' : 'restriction';
|
|
347
|
+
addChange(changes, impact, 'count-changed', field, before, after);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function compareBoolean(changes, field, before, after, trueIsRisk) {
|
|
351
|
+
if (before === after) return;
|
|
352
|
+
const impact = after === trueIsRisk ? 'expansion' : 'restriction';
|
|
353
|
+
addChange(changes, impact, 'boolean-changed', field, before, after);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function compareArray(changes, field, before, after, additionsAreRisk = true) {
|
|
357
|
+
const added = after.filter((item) => !before.includes(item));
|
|
358
|
+
const removed = before.filter((item) => !after.includes(item));
|
|
359
|
+
if (added.length) {
|
|
360
|
+
changes.push({
|
|
361
|
+
impact: additionsAreRisk ? 'expansion' : 'restriction',
|
|
362
|
+
type: 'items-added',
|
|
363
|
+
field,
|
|
364
|
+
items: added
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (removed.length) {
|
|
368
|
+
changes.push({
|
|
369
|
+
impact: additionsAreRisk ? 'restriction' : 'expansion',
|
|
370
|
+
type: 'items-removed',
|
|
371
|
+
field,
|
|
372
|
+
items: removed
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function compareRanked(changes, field, before, after, ranks) {
|
|
378
|
+
if (before === after) return;
|
|
379
|
+
if (!Object.hasOwn(ranks, before) || !Object.hasOwn(ranks, after) || ranks[before] === ranks[after]) {
|
|
380
|
+
addChange(changes, 'review', 'value-changed', field, before, after);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
addChange(
|
|
384
|
+
changes,
|
|
385
|
+
ranks[after] > ranks[before] ? 'expansion' : 'restriction',
|
|
386
|
+
'value-changed',
|
|
387
|
+
field,
|
|
388
|
+
before,
|
|
389
|
+
after
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function compareClaudeMetadata(changes, before, after) {
|
|
394
|
+
compareRanked(changes, 'defaultMode', before.defaultMode, after.defaultMode, {
|
|
395
|
+
plan: 0,
|
|
396
|
+
manual: 0,
|
|
397
|
+
default: 1,
|
|
398
|
+
acceptEdits: 2,
|
|
399
|
+
dontAsk: 3,
|
|
400
|
+
auto: 4,
|
|
401
|
+
bypassPermissions: 5
|
|
402
|
+
});
|
|
403
|
+
compareCount(changes, 'allowRuleCount', before.allowRuleCount, after.allowRuleCount, true);
|
|
404
|
+
compareCount(changes, 'askRuleCount', before.askRuleCount, after.askRuleCount, false);
|
|
405
|
+
compareCount(changes, 'denyRuleCount', before.denyRuleCount, after.denyRuleCount, false);
|
|
406
|
+
compareArray(changes, 'allowCategories', before.allowCategories, after.allowCategories, true);
|
|
407
|
+
compareCount(
|
|
408
|
+
changes,
|
|
409
|
+
'additionalDirectoryCount',
|
|
410
|
+
before.additionalDirectoryCount,
|
|
411
|
+
after.additionalDirectoryCount,
|
|
412
|
+
true
|
|
413
|
+
);
|
|
414
|
+
compareBoolean(changes, 'bypassDisabled', before.bypassDisabled, after.bypassDisabled, false);
|
|
415
|
+
compareBoolean(changes, 'autoModeDisabled', before.autoModeDisabled, after.autoModeDisabled, false);
|
|
416
|
+
compareBoolean(changes, 'outsideReadsBlocked', before.outsideReadsBlocked, after.outsideReadsBlocked, false);
|
|
417
|
+
compareCount(
|
|
418
|
+
changes,
|
|
419
|
+
'uninspectedRootKeyCount',
|
|
420
|
+
before.uninspectedRootKeyCount,
|
|
421
|
+
after.uninspectedRootKeyCount,
|
|
422
|
+
true
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function compareCodexMetadata(changes, before, after) {
|
|
427
|
+
compareRanked(changes, 'approvalPolicy', before.approvalPolicy, after.approvalPolicy, {
|
|
428
|
+
'on-request': 0,
|
|
429
|
+
granular: 0,
|
|
430
|
+
never: 2
|
|
431
|
+
});
|
|
432
|
+
compareArray(
|
|
433
|
+
changes,
|
|
434
|
+
'granularPromptCategories',
|
|
435
|
+
before.granularPromptCategories,
|
|
436
|
+
after.granularPromptCategories,
|
|
437
|
+
false
|
|
438
|
+
);
|
|
439
|
+
compareRanked(changes, 'approvalsReviewer', before.approvalsReviewer, after.approvalsReviewer, {
|
|
440
|
+
user: 0,
|
|
441
|
+
auto_review: 1
|
|
442
|
+
});
|
|
443
|
+
compareRanked(changes, 'sandboxMode', before.sandboxMode, after.sandboxMode, {
|
|
444
|
+
'read-only': 0,
|
|
445
|
+
'workspace-write': 1,
|
|
446
|
+
'danger-full-access': 2
|
|
447
|
+
});
|
|
448
|
+
compareRanked(changes, 'defaultPermissionsKind', before.defaultPermissionsKind, after.defaultPermissionsKind, {
|
|
449
|
+
':read-only': 0,
|
|
450
|
+
':workspace': 1,
|
|
451
|
+
custom: 2,
|
|
452
|
+
':danger-full-access': 3
|
|
453
|
+
});
|
|
454
|
+
compareRanked(changes, 'webSearch', before.webSearch, after.webSearch, {
|
|
455
|
+
disabled: 0,
|
|
456
|
+
cached: 1,
|
|
457
|
+
indexed: 1,
|
|
458
|
+
live: 2
|
|
459
|
+
});
|
|
460
|
+
compareBoolean(changes, 'networkAccess', before.networkAccess, after.networkAccess, true);
|
|
461
|
+
compareCount(
|
|
462
|
+
changes,
|
|
463
|
+
'additionalWritableRootCount',
|
|
464
|
+
before.additionalWritableRootCount,
|
|
465
|
+
after.additionalWritableRootCount,
|
|
466
|
+
true
|
|
467
|
+
);
|
|
468
|
+
compareBoolean(changes, 'tempRootsExcluded', before.tempRootsExcluded, after.tempRootsExcluded, false);
|
|
469
|
+
compareBoolean(changes, 'subagentsEnabled', before.subagentsEnabled, after.subagentsEnabled, true);
|
|
470
|
+
compareCount(
|
|
471
|
+
changes,
|
|
472
|
+
'uninspectedAgentKeyCount',
|
|
473
|
+
before.uninspectedAgentKeyCount,
|
|
474
|
+
after.uninspectedAgentKeyCount,
|
|
475
|
+
true
|
|
476
|
+
);
|
|
477
|
+
compareCount(
|
|
478
|
+
changes,
|
|
479
|
+
'uninspectedRootKeyCount',
|
|
480
|
+
before.uninspectedRootKeyCount,
|
|
481
|
+
after.uninspectedRootKeyCount,
|
|
482
|
+
true
|
|
483
|
+
);
|
|
484
|
+
compareCount(
|
|
485
|
+
changes,
|
|
486
|
+
'uninspectedSectionKindCount',
|
|
487
|
+
before.uninspectedSectionKindCount,
|
|
488
|
+
after.uninspectedSectionKindCount,
|
|
489
|
+
true
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function diffPermissionPostureSnapshots(beforeInput, afterInput) {
|
|
494
|
+
const before = validateSnapshot(beforeInput);
|
|
495
|
+
const after = validateSnapshot(afterInput);
|
|
496
|
+
if (before.runtime !== after.runtime) throw new Error('Cannot compare different permission posture runtimes.');
|
|
497
|
+
|
|
498
|
+
const changes = [];
|
|
499
|
+
if (before.policyDigest !== after.policyDigest) {
|
|
500
|
+
changes.push({ impact: 'review', type: 'policy-changed', field: 'policyDigest' });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
compareArray(changes, 'signals', before.posture.signals, after.posture.signals, true);
|
|
504
|
+
if (before.posture.action !== after.posture.action) {
|
|
505
|
+
addChange(
|
|
506
|
+
changes,
|
|
507
|
+
ACTION_RANK[after.posture.action] > ACTION_RANK[before.posture.action]
|
|
508
|
+
? 'expansion'
|
|
509
|
+
: 'restriction',
|
|
510
|
+
'decision-changed',
|
|
511
|
+
'action',
|
|
512
|
+
before.posture.action,
|
|
513
|
+
after.posture.action
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (before.runtime === 'claude') {
|
|
518
|
+
compareClaudeMetadata(changes, before.posture.metadata, after.posture.metadata);
|
|
519
|
+
} else {
|
|
520
|
+
compareCodexMetadata(changes, before.posture.metadata, after.posture.metadata);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const summary = {
|
|
524
|
+
expansions: changes.filter((change) => change.impact === 'expansion').length,
|
|
525
|
+
restrictions: changes.filter((change) => change.impact === 'restriction').length,
|
|
526
|
+
reviews: changes.filter((change) => change.impact === 'review').length
|
|
527
|
+
};
|
|
528
|
+
const status = summary.expansions || summary.reviews
|
|
529
|
+
? 'needs-review'
|
|
530
|
+
: summary.restrictions ? 'restricted' : 'unchanged';
|
|
531
|
+
return {
|
|
532
|
+
schemaVersion: SNAPSHOT_VERSION,
|
|
533
|
+
runtime: before.runtime,
|
|
534
|
+
status,
|
|
535
|
+
baselineDigest: before.digest,
|
|
536
|
+
currentDigest: after.digest,
|
|
537
|
+
summary,
|
|
538
|
+
changes
|
|
539
|
+
};
|
|
540
|
+
}
|