trustdex 0.7.0 → 0.8.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 +39 -7
- package/README.md +43 -7
- package/bin/trustdex.mjs +78 -9
- package/docs/ARCHITECTURE.ko.md +1 -1
- package/docs/ARCHITECTURE.md +6 -3
- package/docs/CLAUDE.ko.md +25 -2
- package/docs/CLAUDE.md +33 -5
- package/docs/CODEX.ko.md +28 -5
- package/docs/CODEX.md +38 -11
- package/docs/DEFENSIVE_USE_CASE.ko.md +2 -2
- package/docs/DEFENSIVE_USE_CASE.md +5 -3
- package/docs/EVALUATION.ko.md +12 -1
- package/docs/EVALUATION.md +12 -1
- package/docs/ROADMAP.ko.md +3 -2
- package/docs/ROADMAP.md +3 -2
- package/docs/THREAT_MODEL.ko.md +3 -1
- package/docs/THREAT_MODEL.md +3 -1
- package/package.json +2 -2
- package/schemas/permission-posture-diff-v2.schema.json +37 -0
- package/schemas/permission-posture-v2.schema.json +117 -0
- package/src/claude-settings.mjs +81 -0
- package/src/codex-settings.mjs +81 -3
- package/src/posture.mjs +69 -8
|
@@ -54,9 +54,9 @@ ALLOW 결과는 여전히 관찰된 증거에 대한 정책 결과일 뿐입니
|
|
|
54
54
|
|
|
55
55
|
`inspect-claude-settings`는 문서화된 `permissions` 객체를 검사하고 셸, 파일 시스템, 네트워크, MCP 및 서브에이전트 기능의 자동 승인을 보고합니다. 또한 훅, 자격 증명 도우미, 환경 설정, 샌드박스 설정 및 플러그인 로딩의 존재를 값은 보관하지 않은 채 드러냅니다.
|
|
56
56
|
|
|
57
|
-
이는 정적 증거이지 런타임 강제 적용이 아닙니다.
|
|
57
|
+
`inspect-claude-settings-stack`은 명시적으로 전달된 사용자, 프로젝트, 로컬, 세션 및 관리형 소스에 문서화된 우선순위를 적용합니다. 이는 정적 증거이지 런타임 강제 적용이 아닙니다. 소스를 자동으로 찾거나 관리형 설정이 세션에 실제 적용되었음을 증명하지 않으며 Claude Code 샌드박싱을 대체하지 않습니다. 운영자는 실제 런타임 설정을 별도로 확인해야 합니다.
|
|
58
58
|
|
|
59
|
-
`inspect-codex-settings`는 Codex 승인 정책, 검토자 선택, 샌드박스 종류, 네트워크 접근, 추가 쓰기 가능 루트 개수, 웹 검색 모드 및 명시적인 서브에이전트 활성화를 평가합니다. 완전히 파싱하지 않는 사용자 정의 권한, 앱 및 플러그인 표면은 값은 보관하지 않고 보고합니다.
|
|
59
|
+
`inspect-codex-settings`는 Codex 승인 정책, 검토자 선택, 샌드박스 종류, 네트워크 접근, 추가 쓰기 가능 루트 개수, 웹 검색 모드 및 명시적인 서브에이전트 활성화를 평가합니다. 완전히 파싱하지 않는 사용자 정의 권한, 앱 및 플러그인 표면은 값은 보관하지 않고 보고합니다. `inspect-codex-settings-stack`은 전달된 파일에 시스템부터 CLI까지의 문서화된 우선순위와 순서가 있는 프로젝트 계층을 적용합니다. 활성 계층을 자동으로 찾거나 요구사항을 강제하거나 활성 Codex 샌드박스를 대체하지 않습니다.
|
|
60
60
|
|
|
61
61
|
## CVP 신청 경계
|
|
62
62
|
|
|
@@ -54,13 +54,15 @@ An ALLOW result is still only a policy result over observed evidence. TrustDex i
|
|
|
54
54
|
|
|
55
55
|
`inspect-claude-settings` examines the documented `permissions` object and reports automatic approval of shell, filesystem, network, MCP, and subagent capabilities. It also surfaces the presence of hooks, credential helpers, environment settings, sandbox configuration, and plugin loading without retaining their values.
|
|
56
56
|
|
|
57
|
-
This is static evidence, not runtime enforcement. It does not
|
|
57
|
+
`inspect-claude-settings-stack` applies documented precedence to explicitly supplied user, project, local, session, and managed sources. This is static evidence, not runtime enforcement. It does not discover sources or prove that managed settings reached a session, and it does not replace Claude Code sandboxing. The operator must confirm the effective runtime configuration separately.
|
|
58
58
|
|
|
59
59
|
`inspect-codex-settings` evaluates Codex approval policy, reviewer choice, sandbox
|
|
60
60
|
class, network access, additional writable-root count, web-search mode, and explicit
|
|
61
61
|
subagent enablement. It reports custom permission, app, and plugin surfaces that it
|
|
62
|
-
does not fully parse without retaining their values.
|
|
63
|
-
|
|
62
|
+
does not fully parse without retaining their values. `inspect-codex-settings-stack`
|
|
63
|
+
applies documented system-through-CLI precedence to supplied files, including ordered
|
|
64
|
+
project layers. It does not discover the active layers, enforce requirements, or replace
|
|
65
|
+
the active Codex sandbox.
|
|
64
66
|
|
|
65
67
|
## CVP application boundary
|
|
66
68
|
|
package/docs/EVALUATION.ko.md
CHANGED
|
@@ -77,8 +77,19 @@
|
|
|
77
77
|
제거된 권한 규칙 안에서 개수는 같고 내용만 바뀌는 경우는 이 구조적 비교의
|
|
78
78
|
범위 밖입니다.
|
|
79
79
|
|
|
80
|
+
## 병합 설정 스택 사례
|
|
81
|
+
|
|
82
|
+
`test/settings-stack.test.mjs`는 두 런타임의 명시적 소스 스택을 검사합니다. 다음을 요구합니다.
|
|
83
|
+
|
|
84
|
+
- Claude 권한 목록을 결합하면서 관리형/세션/로컬/프로젝트/사용자 스칼라 우선순위를 유지
|
|
85
|
+
- 프로젝트 또는 로컬 Claude 소스가 `auto`나 `bypassPermissions` 기본 모드를 활성화하지 않음
|
|
86
|
+
- Codex 시스템/관리형/사용자/프로필/프로젝트/CLI 우선순위와 가장 가까운 프로젝트 순서 적용
|
|
87
|
+
- 높은 소스가 설정을 대체하더라도 잘못된 낮은 우선순위 입력은 실패
|
|
88
|
+
- 스키마 v2 스냅샷은 고정된 범위 개수만 보관하고 소스 구성 변경에 재검토 요구
|
|
89
|
+
- 소스 경로, 규칙, 도메인, 명령, 쓰기 가능 경로, 환경 변수 이름과 값을 계속 제외
|
|
90
|
+
|
|
80
91
|
## 이 평가가 측정하지 않는 것
|
|
81
92
|
|
|
82
|
-
이 벤치마크는 의도적으로 범위가 좁습니다. 런타임 동작, 샌드박스 탈출, 임의의 악성 코드, 모든 형태의 프롬프트 인젝션, 게시자 신원 소유권 또는 전이 의존성의 취약점을 측정하지 않습니다.
|
|
93
|
+
이 벤치마크는 의도적으로 범위가 좁습니다. 런타임 동작, 샌드박스 탈출, 임의의 악성 코드, 모든 형태의 프롬프트 인젝션, 게시자 신원 소유권 또는 전이 의존성의 취약점을 측정하지 않습니다. 스택 사례는 테스트에 명시적으로 전달된 소스만 병합하며 활성 설정, 요구사항, 환경 변수 재정의 또는 서버 관리 정책을 자동으로 찾지 않고 보고된 샌드박스가 실제로 활성화되었음도 증명하지 않습니다. 특히 이미 실행 중인 MCP 서버가 동적으로 반환하는 도구 설명은 현재 정적 설정 경계 밖에 있습니다.
|
|
83
94
|
|
|
84
95
|
이 작은 픽스처 집합은 회귀 기준선이지 탐지 정확도에 대한 통계적 주장이 아닙니다. 거짓 양성과 거짓 음성 비율을 측정하려면 민감 정보를 제거한 실제 설정으로 구성되고 독립적으로 검토된 더 큰 코퍼스가 필요합니다.
|
package/docs/EVALUATION.md
CHANGED
|
@@ -79,8 +79,19 @@ for both runtimes. `test/posture.test.mjs` requires:
|
|
|
79
79
|
The digest checks internal consistency, not signer identity. Same-count changes inside
|
|
80
80
|
redacted permission rules remain outside this structural comparison.
|
|
81
81
|
|
|
82
|
+
## Merged settings stack cases
|
|
83
|
+
|
|
84
|
+
`test/settings-stack.test.mjs` exercises explicit source stacks for both runtimes. It requires:
|
|
85
|
+
|
|
86
|
+
- Claude permission lists to combine while managed/session/local/project/user scalar precedence is preserved
|
|
87
|
+
- project or local Claude sources not to activate `auto` or `bypassPermissions` default modes
|
|
88
|
+
- Codex system/managed/user/profile/project/CLI precedence and closest-project ordering
|
|
89
|
+
- invalid lower-precedence input to fail even when a higher source would replace the setting
|
|
90
|
+
- schema-v2 snapshots to retain only fixed scope counts and to require review when source topology changes
|
|
91
|
+
- source paths, rules, domains, commands, writable paths, environment names, and values to remain absent
|
|
92
|
+
|
|
82
93
|
## What this does not measure
|
|
83
94
|
|
|
84
|
-
This benchmark is intentionally narrow. It does not measure runtime behavior, sandbox escape, arbitrary malware, every form of prompt injection, publisher identity ownership, or vulnerabilities in transitive dependencies.
|
|
95
|
+
This benchmark is intentionally narrow. It does not measure runtime behavior, sandbox escape, arbitrary malware, every form of prompt injection, publisher identity ownership, or vulnerabilities in transitive dependencies. The stack cases merge only sources explicitly supplied to the test; they do not discover active settings, requirements, environment overrides, or server-managed policy, and they do not prove that the reported sandbox is active. In particular, tool descriptions returned dynamically by an already-running MCP server are outside the current static configuration boundary.
|
|
85
96
|
|
|
86
97
|
The small fixture set is a regression baseline, not a statistical claim about detection accuracy. False-positive and false-negative rates require a larger, independently reviewed corpus of redacted real-world configurations.
|
package/docs/ROADMAP.ko.md
CHANGED
|
@@ -12,6 +12,8 @@ TrustDex는 초기 단계입니다. 이 로드맵은 방향을 공유하며 릴
|
|
|
12
12
|
- TrustDex 0.7.0은 값이 제거되고 자체 일관성을 검증하는 권한 상태 스냅샷,
|
|
13
13
|
자율성 확대 검토 게이트, 버전 지정 JSON 스키마 및 공개 적대적 권한 픽스처를
|
|
14
14
|
추가합니다.
|
|
15
|
+
- TrustDex 0.8.0은 문서화된 Claude Code 및 Codex 우선순위를 위한 명시적 병합 소스
|
|
16
|
+
검사, 스키마 v2 소스 구성 증거 및 안전하게 실패하는 스택 테스트를 추가합니다.
|
|
15
17
|
|
|
16
18
|
## 현재: 경계 강화
|
|
17
19
|
|
|
@@ -24,11 +26,10 @@ TrustDex는 초기 단계입니다. 이 로드맵은 방향을 공유하며 릴
|
|
|
24
26
|
|
|
25
27
|
## 다음: 에이전트 행동 경계를 검토 가능하게 만들기
|
|
26
28
|
|
|
27
|
-
- 명령, 경로, 도메인 또는 자격 증명 값을 보관하지 않고 병합된 설정 소스 검사
|
|
28
|
-
- 초기 단일 파일 권한 상태 비교를 병합 설정 소스로 확장
|
|
29
29
|
- 승인된 도구 출처와 해당 도구에 부여된 행동 권한 연결
|
|
30
30
|
- 버전이 지정된 권한 상태 JSON 스키마를 추가 결정 및 런타임으로 확장
|
|
31
31
|
- 권고형 설정 검사가 런타임 및 운영체제 샌드박스와 결합되는 방식 문서화
|
|
32
|
+
- 비공개 파일을 조용히 찾지 않으면서 런타임이 생성한 실제 설정 증거 추가
|
|
32
33
|
|
|
33
34
|
## 그다음: 무결성 증명
|
|
34
35
|
|
package/docs/ROADMAP.md
CHANGED
|
@@ -12,6 +12,8 @@ TrustDex is early-stage. This roadmap communicates direction and is not a releas
|
|
|
12
12
|
- TrustDex 0.7.0 adds value-free, self-validating permission posture snapshots,
|
|
13
13
|
autonomy-expansion review gates, versioned JSON schemas, and public adversarial
|
|
14
14
|
permission fixtures.
|
|
15
|
+
- TrustDex 0.8.0 adds explicit merged-source inspection for documented Claude Code
|
|
16
|
+
and Codex precedence, schema-v2 source topology evidence, and fail-closed stack tests.
|
|
15
17
|
|
|
16
18
|
## Now: harden the boundary
|
|
17
19
|
|
|
@@ -24,11 +26,10 @@ TrustDex is early-stage. This roadmap communicates direction and is not a releas
|
|
|
24
26
|
|
|
25
27
|
## Next: make agent action boundaries reviewable
|
|
26
28
|
|
|
27
|
-
- inspect merged settings sources without retaining commands, paths, domains, or credential values
|
|
28
|
-
- extend the initial single-file posture comparison to merged settings sources
|
|
29
29
|
- connect approved tool provenance to the action permissions granted to that tool
|
|
30
30
|
- extend the versioned permission-posture JSON schemas to additional decisions and runtimes
|
|
31
31
|
- document how advisory settings checks compose with runtime and operating-system sandboxes
|
|
32
|
+
- add runtime-produced effective-setting evidence without silently discovering private files
|
|
32
33
|
|
|
33
34
|
## Then: prove integrity
|
|
34
35
|
|
package/docs/THREAT_MODEL.ko.md
CHANGED
|
@@ -23,6 +23,7 @@ TrustDex는 하나의 좁은 질문을 다룹니다.
|
|
|
23
23
|
- 샌드박스를 제거하거나 승인을 생략하거나 네트워크 접근 또는 쓰기 가능 루트를 추가하는 Codex 설정
|
|
24
24
|
- 명시적인 검토 없이 변경되는 행동 권한 설정
|
|
25
25
|
- 값이 제거된 구조적 기준 상태보다 확대되는 Claude 또는 Codex 권한 상태
|
|
26
|
+
- 명시적으로 전달된 Claude 또는 Codex 설정 소스 사이의 우선순위 변경
|
|
26
27
|
|
|
27
28
|
## 신뢰 경계
|
|
28
29
|
|
|
@@ -45,7 +46,8 @@ TrustDex는 익숙한 프로젝트 이름, 스타 수, 패키지 이름 또는
|
|
|
45
46
|
- 로컬 신뢰 저장소 주장만으로 게시자 소유권을 독립적으로 증명
|
|
46
47
|
- 완전히 침해된 호스트 보호
|
|
47
48
|
- 도난당한 서명 비공개 키로부터 보호
|
|
48
|
-
- 모든
|
|
49
|
+
- 모든 활성 설정 소스, 프로필, 환경 변수 재정의, 요구사항 또는 서버 전달 정책의 자동 탐색
|
|
50
|
+
- 명시적으로 병합한 설정 투영이 런타임에서 활성화된 정책임을 증명하는 것
|
|
49
51
|
- Claude Code 또는 Codex 행동 권한의 런타임 강제 적용
|
|
50
52
|
- 서명되지 않은 권한 상태 스냅샷 작성자의 인증
|
|
51
53
|
|
package/docs/THREAT_MODEL.md
CHANGED
|
@@ -23,6 +23,7 @@ TrustDex addresses one narrow question:
|
|
|
23
23
|
- Codex configurations that remove the sandbox, suppress approvals, enable network access, or add writable roots
|
|
24
24
|
- action-permission settings changing without an explicit review
|
|
25
25
|
- Claude or Codex permission posture expanding relative to a value-free structural baseline
|
|
26
|
+
- precedence changes across explicitly supplied Claude or Codex settings sources
|
|
26
27
|
|
|
27
28
|
## Trust boundaries
|
|
28
29
|
|
|
@@ -45,7 +46,8 @@ TrustDex does **not** treat a familiar project name, star count, package name, o
|
|
|
45
46
|
- independently proving publisher ownership from a local trust-store claim
|
|
46
47
|
- protecting a fully compromised host
|
|
47
48
|
- protecting a stolen signing private key
|
|
48
|
-
-
|
|
49
|
+
- automatically discovering every active settings source, profile, environment override, requirement, or server-delivered policy
|
|
50
|
+
- proving that the explicitly merged settings projection is the policy active at runtime
|
|
49
51
|
- enforcing Claude Code or Codex action permissions at runtime
|
|
50
52
|
- authenticating the author of an unsigned permission posture snapshot
|
|
51
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trustdex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node --test",
|
|
11
|
-
"test:research": "node --test test/research-fixtures.test.mjs test/claude-settings.test.mjs test/codex-settings.test.mjs test/posture.test.mjs",
|
|
11
|
+
"test:research": "node --test test/research-fixtures.test.mjs test/claude-settings.test.mjs test/codex-settings.test.mjs test/posture.test.mjs test/settings-stack.test.mjs",
|
|
12
12
|
"check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/claude-settings.mjs && node --check src/codex-config.mjs && node --check src/codex-settings.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/posture.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
|
|
13
13
|
"prepublishOnly": "npm test && npm run check"
|
|
14
14
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/permission-posture-diff-v2.schema.json",
|
|
4
|
+
"title": "TrustDex merged permission posture diff v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"runtime",
|
|
10
|
+
"status",
|
|
11
|
+
"baselineDigest",
|
|
12
|
+
"currentDigest",
|
|
13
|
+
"summary",
|
|
14
|
+
"changes"
|
|
15
|
+
],
|
|
16
|
+
"properties": {
|
|
17
|
+
"schemaVersion": { "const": 2 },
|
|
18
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
19
|
+
"status": { "enum": ["unchanged", "restricted", "needs-review"] },
|
|
20
|
+
"baselineDigest": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/digest" },
|
|
21
|
+
"currentDigest": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/digest" },
|
|
22
|
+
"summary": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"required": ["expansions", "restrictions", "reviews"],
|
|
26
|
+
"properties": {
|
|
27
|
+
"expansions": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/count" },
|
|
28
|
+
"restrictions": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/count" },
|
|
29
|
+
"reviews": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/count" }
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"changes": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "$ref": "permission-posture-diff-v1.schema.json#/$defs/change" }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/grigent/trustdex/blob/main/schemas/permission-posture-v2.schema.json",
|
|
4
|
+
"title": "TrustDex merged permission posture snapshot v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"kind",
|
|
10
|
+
"createdAt",
|
|
11
|
+
"runtime",
|
|
12
|
+
"policyDigest",
|
|
13
|
+
"posture",
|
|
14
|
+
"sources",
|
|
15
|
+
"digest"
|
|
16
|
+
],
|
|
17
|
+
"properties": {
|
|
18
|
+
"schemaVersion": { "const": 2 },
|
|
19
|
+
"kind": { "const": "agent-permission-posture" },
|
|
20
|
+
"createdAt": { "type": "string", "format": "date-time" },
|
|
21
|
+
"runtime": { "enum": ["claude", "codex"] },
|
|
22
|
+
"policyDigest": { "$ref": "permission-posture-v1.schema.json#/$defs/digest" },
|
|
23
|
+
"digest": { "$ref": "permission-posture-v1.schema.json#/$defs/digest" },
|
|
24
|
+
"posture": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"additionalProperties": false,
|
|
27
|
+
"required": ["action", "signals", "metadata"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"action": { "enum": ["allow", "ask", "block"] },
|
|
30
|
+
"signals": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"uniqueItems": true,
|
|
33
|
+
"items": { "type": "string" }
|
|
34
|
+
},
|
|
35
|
+
"metadata": { "type": "object" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"sources": { "type": "object" }
|
|
39
|
+
},
|
|
40
|
+
"allOf": [
|
|
41
|
+
{
|
|
42
|
+
"if": { "properties": { "runtime": { "const": "claude" } } },
|
|
43
|
+
"then": {
|
|
44
|
+
"properties": {
|
|
45
|
+
"posture": {
|
|
46
|
+
"properties": {
|
|
47
|
+
"metadata": { "$ref": "permission-posture-v1.schema.json#/$defs/claudeMetadata" }
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"sources": { "$ref": "#/$defs/claudeSources" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"if": { "properties": { "runtime": { "const": "codex" } } },
|
|
56
|
+
"then": {
|
|
57
|
+
"properties": {
|
|
58
|
+
"posture": {
|
|
59
|
+
"properties": {
|
|
60
|
+
"metadata": { "$ref": "permission-posture-v1.schema.json#/$defs/codexMetadata" }
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"sources": { "$ref": "#/$defs/codexSources" }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
"$defs": {
|
|
69
|
+
"count": {
|
|
70
|
+
"type": "integer",
|
|
71
|
+
"minimum": 0
|
|
72
|
+
},
|
|
73
|
+
"claudeSources": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"additionalProperties": false,
|
|
76
|
+
"required": ["sourceCount", "activeSourceCount", "sourceCounts"],
|
|
77
|
+
"properties": {
|
|
78
|
+
"sourceCount": { "$ref": "#/$defs/count" },
|
|
79
|
+
"activeSourceCount": { "$ref": "#/$defs/count" },
|
|
80
|
+
"sourceCounts": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"required": ["user", "project", "local", "session", "managed"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"user": { "$ref": "#/$defs/count" },
|
|
86
|
+
"project": { "$ref": "#/$defs/count" },
|
|
87
|
+
"local": { "$ref": "#/$defs/count" },
|
|
88
|
+
"session": { "$ref": "#/$defs/count" },
|
|
89
|
+
"managed": { "$ref": "#/$defs/count" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"codexSources": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": false,
|
|
97
|
+
"required": ["sourceCount", "activeSourceCount", "sourceCounts"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"sourceCount": { "$ref": "#/$defs/count" },
|
|
100
|
+
"activeSourceCount": { "$ref": "#/$defs/count" },
|
|
101
|
+
"sourceCounts": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"additionalProperties": false,
|
|
104
|
+
"required": ["system", "managed", "user", "profile", "project", "cli"],
|
|
105
|
+
"properties": {
|
|
106
|
+
"system": { "$ref": "#/$defs/count" },
|
|
107
|
+
"managed": { "$ref": "#/$defs/count" },
|
|
108
|
+
"user": { "$ref": "#/$defs/count" },
|
|
109
|
+
"profile": { "$ref": "#/$defs/count" },
|
|
110
|
+
"project": { "$ref": "#/$defs/count" },
|
|
111
|
+
"cli": { "$ref": "#/$defs/count" }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/claude-settings.mjs
CHANGED
|
@@ -19,6 +19,14 @@ const MODES = new Set([
|
|
|
19
19
|
'auto'
|
|
20
20
|
]);
|
|
21
21
|
const NON_PERMISSION_ROOT_KEYS = new Set(['$schema', 'permissions']);
|
|
22
|
+
const CLAUDE_SCOPE_RANK = Object.freeze({
|
|
23
|
+
user: 0,
|
|
24
|
+
project: 1,
|
|
25
|
+
local: 2,
|
|
26
|
+
session: 3,
|
|
27
|
+
managed: 4
|
|
28
|
+
});
|
|
29
|
+
const MERGED_PERMISSION_LISTS = new Set(['allow', 'ask', 'deny', 'additionalDirectories']);
|
|
22
30
|
|
|
23
31
|
function isObject(value) {
|
|
24
32
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -171,3 +179,76 @@ export function inspectClaudeSettings(settings, sourceName = 'settings.json') {
|
|
|
171
179
|
envKeys
|
|
172
180
|
};
|
|
173
181
|
}
|
|
182
|
+
|
|
183
|
+
function normalizeClaudeLayers(layers) {
|
|
184
|
+
if (!Array.isArray(layers) || !layers.length) {
|
|
185
|
+
throw new Error('Claude settings stack requires at least one settings source.');
|
|
186
|
+
}
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
return layers.map((layer, index) => {
|
|
189
|
+
if (!isObject(layer) || !Object.hasOwn(CLAUDE_SCOPE_RANK, layer.scope)) {
|
|
190
|
+
throw new Error('Unsupported Claude settings source scope.');
|
|
191
|
+
}
|
|
192
|
+
if (seen.has(layer.scope)) {
|
|
193
|
+
throw new Error(`Claude settings stack accepts only one ${layer.scope} source.`);
|
|
194
|
+
}
|
|
195
|
+
seen.add(layer.scope);
|
|
196
|
+
if (!isObject(layer.settings)) throw new Error('Claude settings source must contain a JSON object.');
|
|
197
|
+
inspectClaudeSettings(layer.settings, 'Claude settings source');
|
|
198
|
+
return { ...layer, index };
|
|
199
|
+
}).sort((left, right) =>
|
|
200
|
+
CLAUDE_SCOPE_RANK[left.scope] - CLAUDE_SCOPE_RANK[right.scope] || left.index - right.index
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mergeClaudeSettings(layers) {
|
|
205
|
+
const merged = {};
|
|
206
|
+
const permissions = {};
|
|
207
|
+
|
|
208
|
+
for (const { scope, settings } of layers) {
|
|
209
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
210
|
+
if (key === 'permissions') continue;
|
|
211
|
+
if (Array.isArray(value) && Array.isArray(merged[key])) merged[key] = [...merged[key], ...value];
|
|
212
|
+
else merged[key] = value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const current = settings.permissions || {};
|
|
216
|
+
for (const [key, value] of Object.entries(current)) {
|
|
217
|
+
if (MERGED_PERMISSION_LISTS.has(key)) {
|
|
218
|
+
permissions[key] = [...(permissions[key] || []), ...value];
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (key === 'defaultMode' && ['project', 'local'].includes(scope) &&
|
|
222
|
+
['auto', 'bypassPermissions'].includes(value)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
permissions[key] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Object.keys(permissions).length) merged.permissions = permissions;
|
|
230
|
+
return merged;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function inspectClaudeSettingsStack(layers) {
|
|
234
|
+
const normalized = normalizeClaudeLayers(layers);
|
|
235
|
+
const merged = mergeClaudeSettings(normalized);
|
|
236
|
+
const result = inspectClaudeSettings(merged, 'merged Claude settings');
|
|
237
|
+
const sourceCounts = Object.fromEntries(Object.keys(CLAUDE_SCOPE_RANK).map((scope) => [scope, 0]));
|
|
238
|
+
let activeSourceCount = 0;
|
|
239
|
+
for (const layer of normalized) {
|
|
240
|
+
sourceCounts[layer.scope] += 1;
|
|
241
|
+
if (Object.keys(layer.settings).length) activeSourceCount += 1;
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
...result,
|
|
245
|
+
name: 'merged Claude settings',
|
|
246
|
+
kind: 'claude-settings-stack',
|
|
247
|
+
source: { type: 'local', format: 'claude-settings-stack', scopes: sourceCounts },
|
|
248
|
+
settingsContext: {
|
|
249
|
+
sourceCount: normalized.length,
|
|
250
|
+
activeSourceCount,
|
|
251
|
+
sourceCounts
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
package/src/codex-settings.mjs
CHANGED
|
@@ -25,6 +25,14 @@ const INSPECTED_ROOT_KEYS = new Set([
|
|
|
25
25
|
'default_permissions',
|
|
26
26
|
'web_search'
|
|
27
27
|
]);
|
|
28
|
+
const CODEX_SCOPE_RANK = Object.freeze({
|
|
29
|
+
system: 0,
|
|
30
|
+
managed: 1,
|
|
31
|
+
user: 2,
|
|
32
|
+
profile: 3,
|
|
33
|
+
project: 4,
|
|
34
|
+
cli: 5
|
|
35
|
+
});
|
|
28
36
|
|
|
29
37
|
function isObject(value) {
|
|
30
38
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -154,8 +162,7 @@ function validateSandbox(sandbox) {
|
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
|
|
158
|
-
const { root, sandbox, agents, sections } = parseTomlAssignments(text);
|
|
165
|
+
function inspectParsedCodexSettings({ root, sandbox, agents, sections }, sourceName, kind) {
|
|
159
166
|
validateSandbox(sandbox);
|
|
160
167
|
|
|
161
168
|
const signals = new Set();
|
|
@@ -222,7 +229,7 @@ export function inspectCodexSettingsText(text, sourceName = 'config.toml') {
|
|
|
222
229
|
|
|
223
230
|
return {
|
|
224
231
|
name: sourceName,
|
|
225
|
-
kind
|
|
232
|
+
kind,
|
|
226
233
|
source: { type: 'local', format: 'codex-settings', path: sourceName },
|
|
227
234
|
metadata: {
|
|
228
235
|
approvalPolicy: approval.mode,
|
|
@@ -245,3 +252,74 @@ export function inspectCodexSettingsText(text, sourceName = 'config.toml') {
|
|
|
245
252
|
envKeys: []
|
|
246
253
|
};
|
|
247
254
|
}
|
|
255
|
+
|
|
256
|
+
export function inspectCodexSettingsText(text, sourceName = 'config.toml') {
|
|
257
|
+
return inspectParsedCodexSettings(
|
|
258
|
+
parseTomlAssignments(text),
|
|
259
|
+
sourceName,
|
|
260
|
+
'codex-settings'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeCodexLayers(layers) {
|
|
265
|
+
if (!Array.isArray(layers) || !layers.length) {
|
|
266
|
+
throw new Error('Codex settings stack requires at least one config source.');
|
|
267
|
+
}
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
return layers.map((layer, index) => {
|
|
270
|
+
if (!isObject(layer) || !Object.hasOwn(CODEX_SCOPE_RANK, layer.scope)) {
|
|
271
|
+
throw new Error('Unsupported Codex config source scope.');
|
|
272
|
+
}
|
|
273
|
+
if (layer.scope !== 'project' && seen.has(layer.scope)) {
|
|
274
|
+
throw new Error(`Codex settings stack accepts only one ${layer.scope} source.`);
|
|
275
|
+
}
|
|
276
|
+
seen.add(layer.scope);
|
|
277
|
+
if (typeof layer.text !== 'string') throw new Error('Codex config source must contain TOML text.');
|
|
278
|
+
const parsed = parseTomlAssignments(layer.text);
|
|
279
|
+
inspectParsedCodexSettings(parsed, 'Codex settings source', 'codex-settings');
|
|
280
|
+
return { ...layer, parsed, index };
|
|
281
|
+
}).sort((left, right) =>
|
|
282
|
+
CODEX_SCOPE_RANK[left.scope] - CODEX_SCOPE_RANK[right.scope] || left.index - right.index
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function inspectCodexSettingsStack(layers) {
|
|
287
|
+
const normalized = normalizeCodexLayers(layers);
|
|
288
|
+
const merged = {
|
|
289
|
+
root: Object.create(null),
|
|
290
|
+
sandbox: Object.create(null),
|
|
291
|
+
agents: Object.create(null),
|
|
292
|
+
sections: []
|
|
293
|
+
};
|
|
294
|
+
const sections = new Set();
|
|
295
|
+
const sourceCounts = Object.fromEntries(Object.keys(CODEX_SCOPE_RANK).map((scope) => [scope, 0]));
|
|
296
|
+
let activeSourceCount = 0;
|
|
297
|
+
|
|
298
|
+
for (const layer of normalized) {
|
|
299
|
+
sourceCounts[layer.scope] += 1;
|
|
300
|
+
Object.assign(merged.root, layer.parsed.root);
|
|
301
|
+
Object.assign(merged.sandbox, layer.parsed.sandbox);
|
|
302
|
+
Object.assign(merged.agents, layer.parsed.agents);
|
|
303
|
+
for (const section of layer.parsed.sections) sections.add(section);
|
|
304
|
+
if (Object.keys(layer.parsed.root).length || Object.keys(layer.parsed.sandbox).length ||
|
|
305
|
+
Object.keys(layer.parsed.agents).length || layer.parsed.sections.length) {
|
|
306
|
+
activeSourceCount += 1;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
merged.sections = [...sections].sort();
|
|
310
|
+
|
|
311
|
+
const result = inspectParsedCodexSettings(
|
|
312
|
+
merged,
|
|
313
|
+
'merged Codex settings',
|
|
314
|
+
'codex-settings-stack'
|
|
315
|
+
);
|
|
316
|
+
return {
|
|
317
|
+
...result,
|
|
318
|
+
source: { type: 'local', format: 'codex-settings-stack', scopes: sourceCounts },
|
|
319
|
+
settingsContext: {
|
|
320
|
+
sourceCount: normalized.length,
|
|
321
|
+
activeSourceCount,
|
|
322
|
+
sourceCounts
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|