@lqc123qwe/car-runtime 1.8.0 → 1.10.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.
Files changed (178) hide show
  1. package/README.md +29 -1
  2. package/dist/SHA256SUMS +1 -0
  3. package/dist/authz/authz.d.ts +44 -0
  4. package/dist/authz/authz.js +65 -0
  5. package/dist/car-runtime-1.10.0.tgz +0 -0
  6. package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +825 -0
  9. package/dist/dx/doctor.d.ts +62 -0
  10. package/dist/dx/doctor.js +127 -0
  11. package/dist/governance/dump.d.ts +47 -0
  12. package/dist/governance/dump.js +17 -0
  13. package/dist/host/facade.d.ts +13 -0
  14. package/dist/host/facade.js +154 -0
  15. package/dist/host/hostGateway.d.ts +117 -0
  16. package/dist/host/hostGateway.js +65 -0
  17. package/dist/host/mappings.d.ts +27 -0
  18. package/dist/host/mappings.js +52 -0
  19. package/dist/host/stdio.d.ts +14 -0
  20. package/dist/host/stdio.js +83 -0
  21. package/dist/kernel/context.d.ts +76 -0
  22. package/dist/kernel/context.js +203 -0
  23. package/dist/kernel/events.d.ts +37 -0
  24. package/dist/kernel/events.js +82 -0
  25. package/dist/load/config.d.ts +79 -0
  26. package/dist/load/config.js +285 -0
  27. package/dist/load/loader.d.ts +72 -0
  28. package/dist/load/loader.js +162 -0
  29. package/dist/load/registry.d.ts +44 -0
  30. package/dist/load/registry.js +58 -0
  31. package/dist/load/report.d.ts +98 -0
  32. package/dist/load/report.js +301 -0
  33. package/dist/load/sigGate.d.ts +22 -0
  34. package/dist/load/sigGate.js +68 -0
  35. package/dist/load/sign.d.ts +25 -0
  36. package/dist/load/sign.js +47 -0
  37. package/dist/load/verifier.d.ts +35 -0
  38. package/dist/load/verifier.js +56 -0
  39. package/dist/loop/goal.d.ts +31 -0
  40. package/dist/loop/goal.js +34 -0
  41. package/dist/loop/recover.d.ts +17 -0
  42. package/dist/loop/recover.js +39 -0
  43. package/dist/loop/stop.d.ts +79 -0
  44. package/dist/loop/stop.js +151 -0
  45. package/dist/mcp/gateway.d.ts +96 -0
  46. package/dist/mcp/gateway.js +69 -0
  47. package/dist/mcp/httpClient.d.ts +27 -0
  48. package/dist/mcp/httpClient.js +196 -0
  49. package/dist/mcp/stdioClient.d.ts +8 -0
  50. package/dist/mcp/stdioClient.js +73 -0
  51. package/dist/ptc/budget.d.ts +21 -0
  52. package/dist/ptc/budget.js +36 -0
  53. package/dist/ptc/erasable.d.ts +14 -0
  54. package/dist/ptc/erasable.js +27 -0
  55. package/dist/ptc/runCode.d.ts +65 -0
  56. package/dist/ptc/runCode.js +140 -0
  57. package/dist/ptc/schemaRender.d.ts +15 -0
  58. package/dist/ptc/schemaRender.js +90 -0
  59. package/dist/ptc/sdk.d.ts +27 -0
  60. package/dist/ptc/sdk.js +23 -0
  61. package/dist/ptc/worker-entry.d.ts +1 -0
  62. package/dist/ptc/worker-entry.js +71 -0
  63. package/dist/release-audit.jsonl +4 -0
  64. package/dist/runtime-core/chatStep.d.ts +40 -0
  65. package/dist/runtime-core/chatStep.js +104 -0
  66. package/dist/runtime-core/credentials.d.ts +49 -0
  67. package/dist/runtime-core/credentials.js +118 -0
  68. package/dist/runtime-core/errors.d.ts +30 -0
  69. package/dist/runtime-core/errors.js +36 -0
  70. package/dist/runtime-core/llm.d.ts +76 -0
  71. package/dist/runtime-core/llm.js +553 -0
  72. package/dist/runtime-core/redaction.d.ts +39 -0
  73. package/dist/runtime-core/redaction.js +79 -0
  74. package/dist/runtime-core/telemetry.d.ts +88 -0
  75. package/dist/runtime-core/telemetry.js +332 -0
  76. package/dist/runtime-core/tools.d.ts +15 -0
  77. package/dist/runtime-core/tools.js +12 -0
  78. package/dist/runtime-core/trace.d.ts +55 -0
  79. package/dist/runtime-core/trace.js +137 -0
  80. package/dist/runtime-core/types.d.ts +84 -0
  81. package/dist/runtime-core/types.js +13 -0
  82. package/dist/sandbox/sandbox.d.ts +68 -0
  83. package/dist/sandbox/sandbox.js +153 -0
  84. package/dist/sbom.json +263 -0
  85. package/dist/seam.d.ts +21 -0
  86. package/dist/seam.js +20 -0
  87. package/dist/security/secrets-baseline.d.ts +43 -0
  88. package/dist/security/secrets-baseline.js +176 -0
  89. package/dist/security/secrets.d.ts +45 -0
  90. package/dist/security/secrets.js +138 -0
  91. package/dist/session/export.d.ts +55 -0
  92. package/dist/session/export.js +81 -0
  93. package/dist/session/fork.d.ts +51 -0
  94. package/dist/session/fork.js +63 -0
  95. package/dist/session/format.d.ts +21 -0
  96. package/dist/session/format.js +49 -0
  97. package/dist/session/indexStore.d.ts +44 -0
  98. package/dist/session/indexStore.js +265 -0
  99. package/dist/session/log.d.ts +79 -0
  100. package/dist/session/log.js +157 -0
  101. package/dist/session/store.d.ts +9 -0
  102. package/dist/session/store.js +37 -0
  103. package/dist/telemetry/metrics.d.ts +25 -0
  104. package/dist/telemetry/metrics.js +26 -0
  105. package/package.json +17 -1
  106. package/src/mcp/gateway.ts +10 -2
  107. package/src/mcp/httpClient.ts +23 -5
  108. package/src/runtime-core/trace.ts +5 -0
  109. package/src/seam.ts +21 -0
  110. package/.gitattributes +0 -11
  111. package/.github/workflows/ci.yml +0 -293
  112. package/.github/workflows/release.yml +0 -54
  113. package/adr/ADR-001.md +0 -4
  114. package/adr/ADR-002.md +0 -5
  115. package/adr/ADR-003.md +0 -35
  116. package/adr/host-onboarding.md +0 -68
  117. package/adr/mlps-audit-checklist.md +0 -41
  118. package/adr/win32-spike.md +0 -45
  119. package/docs/plugin-authoring.md +0 -82
  120. package/native/landlock-run/Makefile +0 -19
  121. package/native/landlock-run/landlock-run.c +0 -162
  122. package/scripts/contract-check.ts +0 -56
  123. package/scripts/invariant-n1.ts +0 -96
  124. package/scripts/package-check.ts +0 -29
  125. package/scripts/ptc-baseline/run-baseline.ts +0 -118
  126. package/scripts/release-artifacts.ts +0 -111
  127. package/scripts/release-pipeline.ts +0 -245
  128. package/scripts/secrets-baseline.ts +0 -33
  129. package/scripts/secrets-scan.ts +0 -56
  130. package/scripts/win32-ffi-debug.ts +0 -143
  131. package/scripts/win32-koffi-spike.ts +0 -537
  132. package/test/contract/eventmap.golden.json +0 -7
  133. package/test/fixtures/hello-plugin.ts +0 -5
  134. package/test/kernel.spec.ts +0 -136
  135. package/test/registry-e2e/run-e2e.ts +0 -213
  136. package/test/s11.spec.ts +0 -129
  137. package/test/s12.spec.ts +0 -135
  138. package/test/s13.spec.ts +0 -117
  139. package/test/s14.spec.ts +0 -117
  140. package/test/s15.spec.ts +0 -80
  141. package/test/s16.spec.ts +0 -133
  142. package/test/s17.spec.ts +0 -102
  143. package/test/s18.spec.ts +0 -81
  144. package/test/s19.spec.ts +0 -138
  145. package/test/s2.spec.ts +0 -245
  146. package/test/s20.spec.ts +0 -241
  147. package/test/s21.spec.ts +0 -231
  148. package/test/s22-m7-store-format.spec.ts +0 -135
  149. package/test/s23-m7-export-index.spec.ts +0 -189
  150. package/test/s24-signature-gate.spec.ts +0 -185
  151. package/test/s25-m8-runtime-core.spec.ts +0 -600
  152. package/test/s26-signature-closeout.spec.ts +0 -349
  153. package/test/s27-host-load-wiring.spec.ts +0 -134
  154. package/test/s28-session-index-followup.spec.ts +0 -236
  155. package/test/s29-otel-producer.spec.ts +0 -252
  156. package/test/s3.spec.ts +0 -179
  157. package/test/s30-release-artifacts.spec.ts +0 -127
  158. package/test/s31-real-model-wiring.spec.ts +0 -329
  159. package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
  160. package/test/s33-anthropic-adapter.spec.ts +0 -354
  161. package/test/s34-mcp-remote-transport.spec.ts +0 -250
  162. package/test/s35-config-ts-zod.spec.ts +0 -145
  163. package/test/s36-host-tool-surface.spec.ts +0 -250
  164. package/test/s37-projection-dedupe.spec.ts +0 -161
  165. package/test/s38-config-jiti.spec.ts +0 -116
  166. package/test/s39-otlp-span-semantics.spec.ts +0 -100
  167. package/test/s4.spec.ts +0 -169
  168. package/test/s40-metrics-cumulative.spec.ts +0 -67
  169. package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
  170. package/test/s42-traceparent-propagation.spec.ts +0 -415
  171. package/test/s5.spec.ts +0 -95
  172. package/test/s6.spec.ts +0 -169
  173. package/test/s7.spec.ts +0 -136
  174. package/test/s7b-baseline.spec.ts +0 -20
  175. package/test/s9.spec.ts +0 -89
  176. package/test/sandbox-escape/j17-orchestrate.sh +0 -30
  177. package/test/sandbox-escape/run-matrix.sh +0 -98
  178. package/tsconfig.json +0 -18
@@ -1,537 +0,0 @@
1
- /**
2
- * Windows 沙箱 spike(S8 骨架;M2系统设计增补 T-3 / adr/win32-spike.md)
3
- *
4
- * M5-S24 阶段 1 实装(2026-09-09):判据 1 自动断言——
5
- * OpenProcessToken(TOKEN_ASSIGN_PRIMARY|TOKEN_DUPLICATE|TOKEN_QUERY)
6
- * → CreateRestrictedToken(DISABLE_MAX_PRIVILEGE + Administrators SID → SidsToDisable)
7
- * → CreateProcessAsUserW → 子进程 `whoami /groups /fo list` + `whoami /priv /fo list`
8
- * → 与父进程基线做差分断言(deny-only + 特权剥离,双证据)。
9
- *
10
- * M5-S25 阶段 2 实装(2026-09-09):判据 3 自动断言——
11
- * CreateJobObjectW → SetInformationJobObject(JobObjectExtendedLimitInformation:
12
- * ProcessMemoryLimit=256MB + ActiveProcessLimit=32 + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
13
- * → 受限子进程(CREATE_SUSPENDED)AssignProcessToJobObject → ResumeThread → IsProcessInJob 复核
14
- * → fork 炸弹探针:并发 spawn 64 个 node 子进程(> ActiveProcessLimit=32),超限创建被拒
15
- * → KILL_ON_JOB_CLOSE 击杀验证:关 Job 句柄后 30s 长睡子进程在 15s 内被终止(父进程存活)
16
- * (fork 炸弹口径 = POC-4 T-17 同源:超限创建在配额内被拒,adr/win32-spike.md 阶段 2)
17
- *
18
- * M5-S26 阶段 3 实装(2026-09-10):判据 2 自动断言(同进程写探针)——
19
- * 受限子进程内:写沙箱允许区(%TEMP% 会话目录)必须成功;写工作区外管理员门控路径
20
- * (C:\Windows\)必须显式 ACCESS_DENIED(EPERM/EACCES,fail-closed 非静默);
21
- * 未受限 token 子进程作对照组(本机父进程非提权,对照组同拒属预期,已如实记录)。
22
- * 四判据至此全部自动断言整合:VERDICT=FOUR-CRITERIA-ALL-PASS 时退出码 0,任一 FAIL 退出码 1
23
- * (CI 门禁直连)。DEC-2 已裁决接入 Windows CI 矩阵:ci.yml win32-spike job(windows-latest)。
24
- *
25
- * 判据 4(koffi 纯 JS 依赖链)随 koffi 可加载 + prebuilt 在位即断言。
26
- *
27
- * koffi 为 optionalDependencies:未安装(非 Windows 或沙箱内无 npm)时输出 SKIP + 判据清单,
28
- * 与 POC-4 windows-skip 后端口径一致(verdict=SKIPPED 不满足发布门禁)。
29
- *
30
- * ABI 备注(koffi 3.2.1 本机探针已核实):
31
- * - koffi.struct 默认 C 自然对齐(SID_AND_ATTRIBUTES=16B,与 Win32 一致);
32
- * - Node Buffer 可直接作 `void *` 参数传入(调用期 pin 住);null 传 `void *` 合法;
33
- * - JS 对象数组不能直传 `void *`(报 ambiguous),须以 koffi.struct 声明参数类型;
34
- * - HANDLE 以 uint64 + BigInt 传参(x64 ABI 下与指针寄存器等价);
35
- * - JOBOBJECT_EXTENDED_LIMIT_INFORMATION x64 = 144B(BASIC_LIMIT 64B + IO_COUNTERS 48B + 4 个 SIZE_T)。
36
- */
37
- import { spawnSync } from 'node:child_process'
38
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
39
- import { tmpdir } from 'node:os'
40
- import path from 'node:path'
41
-
42
- // ---------------- Win32 常量 ----------------
43
- const TOKEN_ASSIGN_PRIMARY = 0x0001
44
- const TOKEN_DUPLICATE = 0x0002
45
- const TOKEN_QUERY = 0x0008
46
- const DISABLE_MAX_PRIVILEGE = 0x00000001
47
- const CREATE_NO_WINDOW = 0x08000000
48
- const CREATE_SUSPENDED = 0x00000004
49
- const CREATE_UNICODE_ENVIRONMENT = 0x00000400
50
- const DETACHED_PROCESS = 0x00000008
51
- const WAIT_TIMEOUT = 0x00000102
52
- const STILL_ACTIVE = 259
53
- const PSEUDO_CURRENT_PROCESS = BigInt.asUintN(64, -1n) // GetCurrentProcess() 伪句柄
54
-
55
- // Job Object(JobObjectExtendedLimitInformation = 9)
56
- const JobObjectExtendedLimitInformation = 9
57
- const JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008
58
- const JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100
59
- const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
60
- const JOB_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024 // 256MB(规划口径)
61
- const JOB_ACTIVE_PROCESS_LIMIT = 32 // 规划口径
62
-
63
- const ADMIN_SID = 'S-1-5-32-544' // BUILTIN\Administrators
64
- const DENY_ONLY_RE = /Deny[- ]only|仅用于拒绝|只用于拒绝|用于拒绝/i // zh-CN / en-US whoami 属性文本(含 "Group used for deny only" 变体)
65
- const PRIV_NAME_RE = /Se[A-Z][A-Za-z]+Privilege/g // 特权名不做本地化,可靠
66
- const DANGEROUS_PRIVS = [
67
- 'SeDebugPrivilege', 'SeBackupPrivilege', 'SeRestorePrivilege',
68
- 'SeTakeOwnershipPrivilege', 'SeLoadDriverPrivilege', 'SeSecurityPrivilege',
69
- 'SeManageVolumePrivilege', 'SeTrustedCredManAccessPrivilege',
70
- ]
71
-
72
- const SystemRoot = process.env.SystemRoot ?? 'C:\\Windows'
73
- const WHOAMI_EXE = path.join(SystemRoot, 'System32', 'whoami.exe')
74
-
75
- type CriterionStatus = 'PASS' | 'FAIL' | 'PENDING' | 'SKIP'
76
- interface Criterion { id: number; name: string; status: CriterionStatus; evidence: string }
77
-
78
- const criteria: Criterion[] = [
79
- { id: 1, name: '受限 token 下 whoami /groups 显示 Administrators 剥离(deny-only)+ 特权剥离', status: 'PENDING', evidence: '' },
80
- { id: 2, name: '工作区外写操作 ACCESS_DENIED(fail-closed,非静默)', status: 'PENDING', evidence: '' },
81
- { id: 3, name: 'Job Object 配额触发:fork 炸弹被拒/被杀、父进程存活', status: 'PENDING', evidence: '' },
82
- { id: 4, name: 'koffi 纯 JS 依赖链(零原生编译,prebuilt 在位)', status: 'PENDING', evidence: '' },
83
- ]
84
-
85
- // Administrators SID(S-1-5-32-544):Revision=1, Count=2, Authority={0,0,0,0,0,5}, Sub=[32,544]
86
- function buildAdministratorsSid(): Buffer {
87
- const b = Buffer.alloc(16) // sizeof(SID) x64 = 16
88
- b[0] = 1
89
- b[1] = 2
90
- b.set([0, 0, 0, 0, 0, 5], 2)
91
- b.writeUInt32LE(32, 8)
92
- b.writeUInt32LE(544, 12)
93
- return b
94
- }
95
-
96
- // JOBOBJECT_EXTENDED_LIMIT_INFORMATION(x64 = 144B),manual 布局写入
97
- function buildExtendedLimitInfo(): Buffer {
98
- const b = Buffer.alloc(144)
99
- b.writeUInt32LE(
100
- JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_PROCESS_MEMORY | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
101
- 16, // BasicLimitInformation.LimitFlags
102
- )
103
- b.writeUInt32LE(JOB_ACTIVE_PROCESS_LIMIT, 40) // BasicLimitInformation.ActiveProcessLimit
104
- b.writeBigUInt64LE(BigInt(JOB_MEMORY_LIMIT_BYTES), 112) // ProcessMemoryLimit
105
- return b
106
- }
107
-
108
- // 显式 Unicode 环境块:CreateProcessAsUserW 传 NULL 环境时,受限子进程可能因缺
109
- // SystemRoot/SystemDrive 等关键变量而 0xC0000142(STATUS_DLL_INIT_FAILED)——
110
- // windows-2025 runner(提权上下文)实测踩坑,本机非提权交互环境不可复现
111
- function buildEnvBlock(): Buffer {
112
- const env: Record<string, string> = {}
113
- for (const [k, v] of Object.entries(process.env)) if (v !== undefined) env[k] = v
114
- if (env.SystemRoot === undefined) env.SystemRoot = SystemRoot
115
- if (env.windir === undefined) env.windir = SystemRoot
116
- if (env.SystemDrive === undefined) env.SystemDrive = 'C:'
117
- return Buffer.from(Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le')
118
- }
119
-
120
- // 输出解码:优先 UTF-8,含替换符则回退 GBK(zh-CN whoami 重定向输出走 OEM 代码页)
121
- function decodeOutput(buf: Buffer): string {
122
- const utf8 = buf.toString('utf8')
123
- return utf8.includes('\uFFFD') ? new TextDecoder('gbk').decode(buf) : utf8
124
- }
125
-
126
- // /fo list 格式:SID 行之后的下一条「属性/Attributes:」行即该组属性
127
- function parseGroupAttrs(listText: string, sid: string): string | null {
128
- const lines = listText.split(/\r?\n/)
129
- for (let i = 0; i < lines.length; i++) {
130
- if (!lines[i].includes(sid)) continue
131
- for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
132
- const l = lines[j].trim()
133
- const m = l.match(/^(?:属性|Attributes)\s*[::]\s*(.+)$/i)
134
- if (m) return m[1]
135
- if (!l) continue
136
- if (/[::]/.test(l)) break // 已进入下一条目(组名/类型/SID 标签行)
137
- }
138
- }
139
- return null
140
- }
141
-
142
- function mandatoryLabel(listText: string): string | null {
143
- const m = listText.match(/S-1-16-\d+/g)
144
- return m ? m[m.length - 1] : null
145
- }
146
-
147
- function privilegeNames(listText: string): string[] {
148
- return [...listText.matchAll(PRIV_NAME_RE)].map((m) => m[0])
149
- }
150
-
151
- interface RestrictedChild {
152
- hProcess: bigint
153
- hThread: bigint
154
- pid: number
155
- resume: () => void
156
- wait: (ms?: number) => number
157
- exitCode: () => number
158
- outText: () => string
159
- cleanup: () => void
160
- }
161
-
162
- async function main() {
163
- console.log('=== CAR win32-koffi spike(M5-S24/S25/S26:四判据自动断言)===')
164
- if (process.platform !== 'win32') {
165
- console.log(`SKIP: spike requires win32 (current: ${process.platform})`)
166
- for (const c of criteria) c.status = 'SKIP'
167
- printReport()
168
- return
169
- }
170
-
171
- let koffi: any
172
- try { koffi = (await import('koffi')).default ?? (await import('koffi') as any) } catch { koffi = null }
173
- if (!koffi) {
174
- console.log('SKIP: koffi not installed(npm install koffi 后重跑)')
175
- console.log('判据清单(adr/win32-spike.md 四判据,见脚本头注释)')
176
- for (const c of criteria) c.status = 'SKIP'
177
- printReport()
178
- return
179
- }
180
-
181
- // ---- 判据 4:koffi prebuilt 在位、零原生编译 ----
182
- // koffi 3.x 的本机模块走平台包 @koromix/koffi-win32-x64(npm install 即得,无 node-gyp 编译)
183
- const repoRoot = path.join(import.meta.dirname ?? '.', '..')
184
- const prebuiltPkg = path.join(repoRoot, 'node_modules', '@koromix', 'koffi-win32-x64', 'win32_x64', 'koffi.node')
185
- const prebuiltBuild = path.join(repoRoot, 'node_modules', 'koffi', 'build', 'koffi', 'win32_x64')
186
- criteria[3].status = 'PASS'
187
- criteria[3].evidence = `koffi 3.2.1 经 optionalDependencies 加载成功;prebuilt 本机模块在位=${existsSync(prebuiltPkg) || existsSync(prebuiltBuild)}(@koromix/koffi-win32-x64 平台包,无编译步骤)`
188
-
189
- // ---- FFI 绑定 ----
190
- const kernel32 = koffi.load('kernel32.dll')
191
- const advapi32 = koffi.load('advapi32.dll')
192
- // SID_AND_ATTRIBUTES 需显式 struct 类型:void * 参数收 JS 对象数组会被判歧义,
193
- // 声明为 SID_AND_ATTRIBUTES * 后 koffi 按 C 布局(16B,x64 自然对齐,探针已核实)转换
194
- const SID_AND_ATTRIBUTES = koffi.struct('SID_AND_ATTRIBUTES', { Sid: 'void *', Attributes: 'uint32' })
195
- const CloseHandle = kernel32.func('int32 __stdcall CloseHandle(uint64 hObject)')
196
- const OpenProcess = kernel32.func('uint64 __stdcall OpenProcess(uint32 dwDesiredAccess, int32 bInheritHandle, uint32 dwProcessId)')
197
- const CreateFileW = kernel32.func('uint64 __stdcall CreateFileW(void *lpFileName, uint32 dwDesiredAccess, uint32 dwShareMode, void *lpSecurityAttributes, uint32 dwCreationDisposition, uint32 dwFlagsAndAttributes, uint64 hTemplateFile)')
198
- const WaitForSingleObject = kernel32.func('uint32 __stdcall WaitForSingleObject(uint64 hHandle, uint32 dwMilliseconds)')
199
- const ResumeThread = kernel32.func('uint32 __stdcall ResumeThread(uint64 hThread)')
200
- const GetExitCodeProcess = kernel32.func('int32 __stdcall GetExitCodeProcess(uint64 hProcess, void *lpExitCode)')
201
- const OpenProcessToken = advapi32.func('int32 __stdcall OpenProcessToken(uint64 ProcessHandle, uint32 DesiredAccess, void *TokenHandle)')
202
- const GetTokenInformation = advapi32.func('int32 __stdcall GetTokenInformation(uint64 TokenHandle, int32 TokenInformationClass, void *TokenInformation, uint32 TokenInformationLength, void *ReturnLength)')
203
- const CreateRestrictedToken = advapi32.func('int32 __stdcall CreateRestrictedToken(uint64 ExistingTokenHandle, uint32 Flags, uint32 DisableSidCount, SID_AND_ATTRIBUTES *SidsToDisable, uint32 DeletePrivilegeCount, void *PrivilegesToDelete, uint32 RestrictedSidCount, void *SidsToRestrict, void *NewTokenHandle)')
204
- const CreateProcessAsUserW = advapi32.func('int32 __stdcall CreateProcessAsUserW(uint64 hToken, void *lpApplicationName, void *lpCommandLine, void *lpProcessAttributes, void *lpThreadAttributes, int32 bInheritHandles, uint32 dwCreationFlags, void *lpEnvironment, void *lpCurrentDirectory, void *lpStartupInfo, void *lpProcessInformation)')
205
- // Job Object(S25 阶段 2)
206
- const CreateJobObjectW = kernel32.func('uint64 __stdcall CreateJobObjectW(void *lpJobAttributes, void *lpName)')
207
- const SetInformationJobObject = kernel32.func('int32 __stdcall SetInformationJobObject(uint64 hJob, int32 JobObjectInformationClass, void *lpJobObjectInformation, uint32 cbJobObjectInformationLength)')
208
- const AssignProcessToJobObject = kernel32.func('int32 __stdcall AssignProcessToJobObject(uint64 hJob, uint64 hProcess)')
209
- const IsProcessInJob = kernel32.func('int32 __stdcall IsProcessInJob(uint64 hProcess, uint64 hJob, void *pbResult)')
210
-
211
- const closeQuiet = (h: bigint) => { try { if (h !== 0n) CloseHandle(h) } catch { /* noop */ } }
212
- // koffi uint64 返回值在安全整数范围内可能给 Number(>2^53 才给 BigInt),统一归一化为 BigInt
213
- const asBig = (v: bigint | number): bigint => (typeof v === 'bigint' ? v : BigInt(Math.round(v)))
214
-
215
- // ---- 受限子进程工厂(无 cmd 中转:lpApplicationName 直指程序 + STARTF_USESTDHANDLES
216
- // 文件句柄重定向,输出捕获内核级、无引号解析/无控制台依赖)----
217
- const createRestrictedChild = (hToken: bigint, program: string, args: string[], opts?: { suspended?: boolean }): RestrictedChild => {
218
- const dir = mkdtempSync(path.join(tmpdir(), 'car-spike-'))
219
- const outFile = path.join(dir, 'out.txt')
220
- // 可继承句柄安全属性:SECURITY_ATTRIBUTES { nLength=24, lpSecurityDescriptor=NULL, bInheritHandle=TRUE }
221
- const sa = Buffer.alloc(24)
222
- sa.writeUInt32LE(24, 0)
223
- sa.writeUInt32LE(1, 16)
224
- const nameBuf = Buffer.from(outFile + '\0', 'utf16le')
225
- const hFile = asBig(CreateFileW(nameBuf, 0x40000000 /* GENERIC_WRITE */, 0x1 /* FILE_SHARE_READ */, sa, 2 /* CREATE_ALWAYS */, 0x80 /* FILE_ATTRIBUTE_NORMAL */, 0n))
226
- if (hFile === BigInt.asUintN(64, -1n)) {
227
- rmSync(dir, { recursive: true, force: true })
228
- throw new Error('CreateFileW failed for probe output file')
229
- }
230
- const appBuf = Buffer.from(program + '\0', 'utf16le')
231
- const q = (a: string) => (a.includes(' ') ? `"${a}"` : a)
232
- const cmdBuf = Buffer.from(`"${program}" ${args.map(q).join(' ')}\0`, 'utf16le')
233
- const si = Buffer.alloc(104) // sizeof(STARTUPINFOW) x64
234
- si.writeUInt32LE(104, 0) // cb
235
- si.writeUInt32LE(0x100, 60) // dwFlags = STARTF_USESTDHANDLES
236
- si.writeBigUInt64LE(0n, 80) // hStdInput
237
- si.writeBigUInt64LE(hFile, 88) // hStdOutput
238
- si.writeBigUInt64LE(hFile, 96) // hStdError
239
- const pi = Buffer.alloc(24) // sizeof(PROCESS_INFORMATION) x64
240
- // DETACHED_PROCESS:不分配控制台。CREATE_NO_WINDOW 仍会 spawn conhost,
241
- // windows-2025 提权会话上受限 token 控制台初始化失败 → 0xC0000142(FFI debug 矩阵实测);
242
- // 输出经 STARTF_USESTDHANDLES 落盘,无控制台需求
243
- const flags = DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT | (opts?.suspended ? CREATE_SUSPENDED : 0)
244
- if (!CreateProcessAsUserW(hToken, appBuf, cmdBuf, null, null, 1 /* bInheritHandles */, flags, buildEnvBlock(), null, si, pi)) {
245
- closeQuiet(hFile)
246
- rmSync(dir, { recursive: true, force: true })
247
- throw new Error('CreateProcessAsUserW failed (rc=0)——可能缺 SeIncreaseQuotaPrivilege 或受宿主沙箱拦截')
248
- }
249
- const hProcess = pi.readBigUInt64LE(0)
250
- const hThread = pi.readBigUInt64LE(8)
251
- const codeBuf = Buffer.alloc(4)
252
- return {
253
- hProcess,
254
- hThread,
255
- pid: pi.readUInt32LE(16),
256
- resume: () => { ResumeThread(hThread) },
257
- wait: (ms = 60000) => WaitForSingleObject(hProcess, ms),
258
- exitCode: () => { GetExitCodeProcess(hProcess, codeBuf); return codeBuf.readUInt32LE(0) },
259
- outText: () => (existsSync(outFile) ? decodeOutput(readFileSync(outFile)) : ''),
260
- cleanup: () => { closeQuiet(hThread); closeQuiet(hProcess); closeQuiet(hFile); rmSync(dir, { recursive: true, force: true }) },
261
- }
262
- }
263
-
264
- // ---- 父进程基线(未受限)----
265
- const parentGroups = decodeOutput(spawnSync(WHOAMI_EXE, ['/groups', '/fo', 'list'], { encoding: 'buffer' }).stdout as Buffer)
266
- const parentPrivsText = decodeOutput(spawnSync(WHOAMI_EXE, ['/priv', '/fo', 'list'], { encoding: 'buffer' }).stdout as Buffer)
267
- const parentAdminAttrs = parseGroupAttrs(parentGroups, ADMIN_SID)
268
- const parentPrivs = privilegeNames(parentPrivsText)
269
-
270
- // ---- 阶段 1 调用链 ----
271
- const hTokBuf = Buffer.alloc(8)
272
- if (!OpenProcessToken(PSEUDO_CURRENT_PROCESS, TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY, hTokBuf)) {
273
- throw new Error('OpenProcessToken failed (rc=0)')
274
- }
275
- const hToken = hTokBuf.readBigUInt64LE(0)
276
-
277
- const sidBuf = buildAdministratorsSid()
278
- const saaArray = [{ Sid: sidBuf, Attributes: 0 }] // SID_AND_ATTRIBUTES[1],Attributes 入参不参与语义
279
- const hRestrBuf = Buffer.alloc(8)
280
- const okRestrict = CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 1, saaArray, 0, null, 0, null, hRestrBuf)
281
- if (!okRestrict) {
282
- closeQuiet(hToken)
283
- throw new Error('CreateRestrictedToken failed (rc=0)——检查 SID_AND_ATTRIBUTES ABI(期望 16B C 对齐)')
284
- }
285
- const hRestricted = hRestrBuf.readBigUInt64LE(0)
286
-
287
- try {
288
- // ========== 判据 1(S24 阶段 1)==========
289
- // 文本探针(whoami)仅作辅助证据:runner 提权会话上受限 token 自查 /groups 会
290
- // Access denied(预期行为),主断言由父侧代读子进程 token 完成(见判据 3 块内)
291
- const c1a = createRestrictedChild(hRestricted, WHOAMI_EXE, ['/groups', '/fo', 'list'])
292
- const c1p = createRestrictedChild(hRestricted, WHOAMI_EXE, ['/priv', '/fo', 'list'])
293
- c1a.wait(); c1p.wait()
294
- const childGroupsRes = { out: c1a.outText(), exitCode: c1a.exitCode() }
295
- const childPrivsRes = { out: c1p.outText(), exitCode: c1p.exitCode() }
296
- c1a.cleanup(); c1p.cleanup()
297
- const childAdminAttrs = parseGroupAttrs(childGroupsRes.out, ADMIN_SID)
298
- const childPrivs = privilegeNames(childPrivsRes.out)
299
- const stripped = parentPrivs.filter((p) => !childPrivs.includes(p))
300
- const childDangerous = childPrivs.filter((p) => DANGEROUS_PRIVS.includes(p))
301
- const parentAlreadyDeny = parentAdminAttrs !== null && DENY_ONLY_RE.test(parentAdminAttrs)
302
- const childDeny = childAdminAttrs !== null && DENY_ONLY_RE.test(childAdminAttrs)
303
-
304
- // ========== 判据 3(S25 阶段 2:Job Object 配额 + fork 炸弹,POC-4 T-17 同源)==========
305
- const hJob = asBig(CreateJobObjectW(null, null))
306
- if (hJob === 0n) throw new Error('CreateJobObjectW failed (returned NULL)')
307
- const okJobInfo = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, buildExtendedLimitInfo(), 144)
308
- if (!okJobInfo) throw new Error('SetInformationJobObject failed (rc=0)——检查 JOBOBJECT_EXTENDED_LIMIT_INFORMATION 布局(期望 144B)')
309
-
310
- // fork 炸弹探针:并发 spawn 64 个 3s 长睡 node 子进程,统计超限被拒数
311
- const forkProbeJs = [
312
- 'const { spawn } = require("child_process");',
313
- 'const N = 64;',
314
- 'let ok = 0, fail = 0, done = 0;',
315
- 'const samples = [];',
316
- 'function finish(extra) { console.log("FORK_RESULT " + JSON.stringify(Object.assign({ ok: ok, fail: fail, samples: samples.slice(0, 5), mem: process.memoryUsage().rss }, extra || {}))); process.exit(0); }',
317
- 'function check() { if (done >= N) finish(); }',
318
- 'for (let i = 0; i < N; i++) {',
319
- ' let c;',
320
- ' try { c = spawn(process.execPath, ["-e", "setTimeout(function(){process.exit(0)},3000)"], { stdio: "ignore", detached: true }); }',
321
- ' catch (e) { fail++; done++; if (samples.length < 5) samples.push({ kind: "throw", msg: String(e && e.message).slice(0, 120) }); continue; }',
322
- ' c.on("error", function (e) { fail++; done++; if (samples.length < 5) samples.push({ kind: "error", msg: String(e && e.code || e).slice(0, 120) }); check(); });',
323
- ' c.on("exit", function (code, sig) { if (code === 0) { ok++; } else { fail++; if (samples.length < 5) samples.push({ kind: "exit", code: code, sig: sig }); } done++; check(); });',
324
- '}',
325
- 'setTimeout(function () { finish({ timeout: true }); }, 20000);',
326
- ].join('\n')
327
- const sleepJs = 'setTimeout(function () { process.exit(0) }, 30000)\n'
328
-
329
- const s25Dir = mkdtempSync(path.join(tmpdir(), 'car-s25-'))
330
- const forkProbePath = path.join(s25Dir, 'fork-probe.js')
331
- const sleepPath = path.join(s25Dir, 'sleep-30s.js')
332
- writeFileSync(forkProbePath, forkProbeJs, 'utf8')
333
- writeFileSync(sleepPath, sleepJs, 'utf8')
334
- const nodeExe = process.execPath
335
-
336
- try {
337
- // ① 长睡哨兵子进程(挂 Job,用于 KILL_ON_JOB_CLOSE 击杀验证 + 判据 1 父侧代读取证)
338
- const sentinel = createRestrictedChild(hRestricted, nodeExe, [sleepPath], { suspended: true })
339
- const okAssignSentinel = AssignProcessToJobObject(hJob, sentinel.hProcess)
340
- sentinel.resume()
341
-
342
- // ①' 判据 1 主断言(父侧代读):受限 token 自查 whoami /groups 在提权会话上会
343
- // Access denied,故由父进程(High IL admin)打开哨兵进程读取其 token 的
344
- // TokenGroups/TokenPrivileges,机器级断言 Administrators SID = deny-only
345
- const SE_GROUP_USE_FOR_DENY_ONLY = 0x10
346
- const SE_GROUP_ENABLED_MASK = 0x2 | 0x4
347
- const readTokenGroups = (token: bigint): Array<{ sid: string; attrs: number }> | null => {
348
- const lenBuf = Buffer.alloc(4)
349
- GetTokenInformation(token, 2 /* TokenGroups */, null, 0, lenBuf)
350
- const len = lenBuf.readUInt32LE(0)
351
- if (len <= 8) return null
352
- const buf = Buffer.alloc(len)
353
- if (!GetTokenInformation(token, 2, buf, len, lenBuf)) return null
354
- const count = buf.readUInt32LE(0)
355
- if (count === 0 || 8 + count * 16 + 8 > len) return null
356
- const sidPtrs: bigint[] = []
357
- for (let i = 0; i < count; i++) sidPtrs.push(buf.readBigUInt64LE(8 + i * 16))
358
- // 内核将 SID blob 连续放在 entries 数组之后:buffer 基址 = min(sidPtr) - (8 + count*16)
359
- const minSid = sidPtrs.reduce((a, b) => (b < a ? b : a))
360
- const base = Number(minSid - BigInt(8 + count * 16))
361
- const out: Array<{ sid: string; attrs: number }> = []
362
- for (let i = 0; i < count; i++) {
363
- const attrs = buf.readUInt32LE(8 + i * 16 + 8)
364
- const off = Number(sidPtrs[i] - BigInt(base)) // 相对 buffer 起点的偏移
365
- if (off < 0 || off + 8 > len) return null
366
- const rev = buf[off]; const subCnt = buf[off + 1]
367
- if (rev !== 1 || subCnt > 15 || off + 8 + subCnt * 4 > len) return null
368
- let auth = 0n
369
- for (let k = 0; k < 6; k++) auth = (auth << 8n) | BigInt(buf[off + 2 + k])
370
- if (auth > 0xffffffffffffn) return null
371
- const subs: number[] = []
372
- for (let k = 0; k < subCnt; k++) subs.push(buf.readUInt32LE(off + 8 + k * 4))
373
- out.push({ sid: `S-1-${auth}-${subs.join('-')}`, attrs })
374
- }
375
- return out
376
- }
377
- const readTokenPrivilegeCount = (token: bigint): number | null => {
378
- const lenBuf = Buffer.alloc(4)
379
- GetTokenInformation(token, 3 /* TokenPrivileges */, null, 0, lenBuf)
380
- const len = lenBuf.readUInt32LE(0)
381
- if (len <= 4) return null
382
- const buf = Buffer.alloc(len)
383
- if (!GetTokenInformation(token, 3, buf, len, lenBuf)) return null
384
- return buf.readUInt32LE(0)
385
- }
386
- const parentTokenGroups = readTokenGroups(hToken)
387
- const parentTokenPrivCount = readTokenPrivilegeCount(hToken)
388
- const parentAdminEntry = parentTokenGroups?.find((g) => g.sid === ADMIN_SID)
389
- let childTokenGroups: Array<{ sid: string; attrs: number }> | null = null
390
- let childTokenPrivCount: number | null = null
391
- const hSentinelProc = asBig(OpenProcess(0x1000 /* PROCESS_QUERY_LIMITED_INFORMATION */, 0, sentinel.pid))
392
- if (hSentinelProc !== 0n) {
393
- const hT = Buffer.alloc(8)
394
- if (OpenProcessToken(hSentinelProc, 0x8 /* TOKEN_QUERY */, hT)) {
395
- const childTok = hT.readBigUInt64LE(0)
396
- childTokenGroups = readTokenGroups(childTok)
397
- childTokenPrivCount = readTokenPrivilegeCount(childTok)
398
- closeQuiet(childTok)
399
- }
400
- closeQuiet(hSentinelProc)
401
- }
402
- const childAdminEntry = childTokenGroups?.find((g) => g.sid === ADMIN_SID)
403
- const childAdminDeny = childAdminEntry !== undefined
404
- && (childAdminEntry.attrs & SE_GROUP_USE_FOR_DENY_ONLY) !== 0
405
- && (childAdminEntry.attrs & SE_GROUP_ENABLED_MASK) === 0
406
- const privDiffOk = childTokenPrivCount !== null && parentTokenPrivCount !== null
407
- && childTokenPrivCount < parentTokenPrivCount
408
- criteria[0].status = childAdminDeny && privDiffOk ? 'PASS' : 'FAIL'
409
- criteria[0].evidence = [
410
- `父 token Administrators: ${parentAdminEntry ? `attrs=0x${parentAdminEntry.attrs.toString(16)}${(parentAdminEntry.attrs & SE_GROUP_ENABLED_MASK) !== 0 ? ' (提权 Enabled)' : ' (UAC filtered deny-only)'}` : '未在列'}`,
411
- `子 token(父侧代读) Administrators: ${childAdminEntry ? `attrs=0x${childAdminEntry.attrs.toString(16)}${childAdminDeny ? ' (USE_FOR_DENY_ONLY)' : ' (非 deny-only!)'}` : '未在列/取证失败'}`,
412
- `TokenPrivileges 计数: 父 ${parentTokenPrivCount ?? '?'} → 子 ${childTokenPrivCount ?? '?'} (DISABLE_MAX_PRIVILEGE)`,
413
- `whoami 辅助证据: 父 Administrators 属性=${parentAdminAttrs ?? '未在列'}, 子=${childAdminAttrs ?? '未在列'}, 特权差分=[${stripped.join(', ') || '无'}]`,
414
- `子 whoami /groups 原文(前160字符): ${childGroupsRes.out.slice(0, 160).replace(/\r?\n/g, ' ⏎ ') || '(空)'}${childGroupsRes.exitCode !== 0 ? ' —— 受限 token 自查被拒,故采用父侧代读取证' : ''}`,
415
- ...(parentAlreadyDeny ? ['注: 本机父进程为 UAC filtered token(父侧已 deny-only),deny-only 主断言不受影响(直接读子 token 属性位)'] : []),
416
- ].join(' | ')
417
-
418
- // ② fork 炸弹子进程(CREATE_SUSPENDED → 挂 Job → Resume,消除子孙脱离 Job 的竞态)
419
- const bomber = createRestrictedChild(hRestricted, nodeExe, [forkProbePath], { suspended: true })
420
- const okAssignBomber = AssignProcessToJobObject(hJob, bomber.hProcess)
421
- bomber.resume()
422
- const inJobBuf = Buffer.alloc(4)
423
- IsProcessInJob(bomber.hProcess, hJob, inJobBuf)
424
- const bomberInJob = inJobBuf.readUInt32LE(0) !== 0
425
-
426
- // ③ 配额内等待 fork 炸弹结束(探针自限时 20s)
427
- const bomberWait = bomber.wait(90000)
428
- const forkLine = bomber.outText().split(/\r?\n/).find((l) => l.startsWith('FORK_RESULT'))
429
- let fork: { ok?: number; fail?: number; timeout?: boolean; samples?: Array<Record<string, unknown>>; mem?: number } = {}
430
- try { fork = JSON.parse((forkLine ?? '').replace(/^FORK_RESULT\s+/, '') || '{}') } catch { /* 解析失败按空处理 */ }
431
-
432
- // ④ KILL_ON_JOB_CLOSE 击杀验证:确认哨兵仍在跑(2s 等待超时=活着),关 Job 句柄,哨兵须在 15s 内被终止
433
- const sentinelAlive = sentinel.wait(2000) === WAIT_TIMEOUT && sentinel.exitCode() === STILL_ACTIVE
434
- CloseHandle(hJob) // 最后一个 Job 用户句柄关闭 → 触发 KILL_ON_JOB_CLOSE
435
- const t0 = Date.now()
436
- const sentinelKilledWait = sentinel.wait(15000)
437
- const killElapsedMs = Date.now() - t0
438
- const sentinelKilled = sentinelKilledWait === 0 && sentinel.exitCode() !== STILL_ACTIVE
439
-
440
- // ⑤ 父进程存活:全部 Job 活动后父进程仍可正常派生子进程
441
- const parentAlive = spawnSync(WHOAMI_EXE, ['/priv'], { encoding: 'buffer' }).status === 0
442
-
443
- const quotaTriggered = (fork.fail ?? 0) > 0
444
- const quotaSane = (fork.ok ?? 0) > 0 // 配额内进程仍可正常创建(非一刀切拒绝)
445
- const c3Pass = okJobInfo && okAssignBomber && bomberInJob && quotaTriggered && quotaSane
446
- && sentinelAlive && sentinelKilled && parentAlive
447
- criteria[2].status = c3Pass ? 'PASS' : 'FAIL'
448
- criteria[2].evidence = [
449
- `Job 限额: ProcessMemory=256MB + ActiveProcess=${JOB_ACTIVE_PROCESS_LIMIT} + KILL_ON_JOB_CLOSE(SetInformationJobObject=${okJobInfo ? 'OK' : 'FAIL'})`,
450
- `fork 探针挂 Job: Assign=${okAssignBomber ? 'OK' : 'FAIL'}, IsProcessInJob=${bomberInJob}`,
451
- `fork 炸弹(64 并发): 成功=${fork.ok ?? '?'} 被拒=${fork.fail ?? '?'}${fork.timeout ? ' (探针超时截断)' : ''}`,
452
- `fork 诊断: 探针RSS=${fork.mem ? Math.round(fork.mem / 1048576) + 'MB' : '?'} 失败样本=${JSON.stringify(fork.samples ?? [])}`,
453
- `KILL_ON_JOB_CLOSE: 哨兵存活确认=${sentinelAlive}, 关句柄后 ${killElapsedMs}ms 内被杀=${sentinelKilled}`,
454
- `父进程存活: ${parentAlive}`,
455
- ...(bomberWait === WAIT_TIMEOUT ? ['告警: fork 探针未在 90s 内退出'] : []),
456
- ].join(' | ')
457
-
458
- sentinel.cleanup()
459
- bomber.cleanup()
460
- rmSync(s25Dir, { recursive: true, force: true })
461
- } catch (e) {
462
- rmSync(s25Dir, { recursive: true, force: true })
463
- throw e
464
- }
465
-
466
- // ========== 判据 2(S26 阶段 3:写探针同进程验证,fail-closed 非静默)==========
467
- // 同进程口径:探针在受限子进程内执行(非父进程代跑);允许区=%TEMP% 会话目录,
468
- // 工作区外=管理员门控路径 C:\Windows\(写权限仅提权进程可获)。
469
- const writeProbeJs = [
470
- 'const fs = require("fs");',
471
- 'const path = require("path");',
472
- 'const res = { insideOk: false, insideErr: null, outsideDenied: false, outsideErr: null, outsideSilent: false };',
473
- 'const insidePath = path.join(process.argv[2], "inside-probe.txt");',
474
- 'try { fs.writeFileSync(insidePath, "car-spike-probe"); fs.unlinkSync(insidePath); res.insideOk = true; }',
475
- 'catch (e) { res.insideErr = e.code || String(e); }',
476
- 'const outsidePath = "C:\\\\Windows\\\\car-spike-outside-" + process.pid + ".tmp";',
477
- 'try { fs.writeFileSync(outsidePath, "car-spike-probe"); res.outsideSilent = true; try { fs.unlinkSync(outsidePath); } catch (e2) {} }',
478
- 'catch (e) { res.outsideDenied = true; res.outsideErr = e.code || String(e); }',
479
- 'console.log("WRITE_RESULT " + JSON.stringify(res));',
480
- ].join('\n')
481
- const s26Dir = mkdtempSync(path.join(tmpdir(), 'car-s26-'))
482
- const writeProbePath = path.join(s26Dir, 'write-probe.js')
483
- writeFileSync(writeProbePath, writeProbeJs, 'utf8')
484
- try {
485
- const runWriteProbe = (tok: bigint) => {
486
- const c = createRestrictedChild(tok, process.execPath, [writeProbePath, s26Dir])
487
- c.wait(60000)
488
- const line = c.outText().split(/\r?\n/).find((l) => l.startsWith('WRITE_RESULT'))
489
- c.cleanup()
490
- try { return JSON.parse((line ?? '').replace(/^WRITE_RESULT\s+/, '') || '{}') } catch { return {} as Record<string, unknown> }
491
- }
492
- const restrictedWrite = runWriteProbe(hRestricted)
493
- const unrestrictedWrite = runWriteProbe(hToken) // 对照组:未受限 token(父进程非提权)
494
- const deniedCodes = ['EPERM', 'EACCES']
495
- const c2Pass = restrictedWrite.insideOk === true
496
- && restrictedWrite.outsideDenied === true
497
- && restrictedWrite.outsideSilent !== true
498
- && deniedCodes.includes(String(restrictedWrite.outsideErr))
499
- criteria[1].status = c2Pass ? 'PASS' : 'FAIL'
500
- criteria[1].evidence = [
501
- `受限子进程写沙箱允许区(%TEMP% 会话目录): ${restrictedWrite.insideOk ? 'OK' : `失败(${restrictedWrite.insideErr})`}`,
502
- `受限子进程写工作区外管理员门控路径(C:\\Windows\\): ${restrictedWrite.outsideDenied ? `ACCESS_DENIED(${restrictedWrite.outsideErr})` : (restrictedWrite.outsideSilent ? '静默成功——fail-closed 破防' : `异常(${restrictedWrite.outsideErr})`)}`,
503
- `对照组(未受限 token 子进程, 父进程非提权): outsideDenied=${unrestrictedWrite.outsideDenied}, err=${unrestrictedWrite.outsideErr}`,
504
- '注: 本机父进程为非提权 UAC filtered token,对照组同拒属预期;强差分=提权终端复跑(父侧可写、受限子侧被拒)。受限 token 已切断管理员写入通道(判据 1: Administrators deny-only + 特权剥离),写失败以显式异常抛出(fail-closed,非静默降级)',
505
- ].join(' | ')
506
- rmSync(s26Dir, { recursive: true, force: true })
507
- } catch (e) {
508
- rmSync(s26Dir, { recursive: true, force: true })
509
- throw e
510
- }
511
- } finally {
512
- closeQuiet(hRestricted)
513
- closeQuiet(hToken)
514
- }
515
-
516
- printReport()
517
- }
518
-
519
- function printReport() {
520
- console.log('')
521
- console.log('---- 四判据状态(adr/win32-spike.md)----')
522
- for (const c of criteria) {
523
- console.log(`判据 ${c.id} [${c.status}] ${c.name}`)
524
- if (c.evidence) console.log(` 证据: ${c.evidence}`)
525
- }
526
- const allPass = criteria.every((c) => c.status === 'PASS')
527
- const verdict = allPass
528
- ? 'FOUR-CRITERIA-ALL-PASS(1.0-GO-2 判据 1+2 达成;1.0-GO-3 判据 3 目标项达成)'
529
- : `SPIKE-INCOMPLETE: ${criteria.filter((c) => c.status !== 'PASS').map((c) => `判据${c.id}=${c.status}`).join(', ')}`
530
- console.log('')
531
- console.log(`VERDICT: ${verdict}`)
532
- // 退出码语义:FAIL=1(真失败,CI 门禁拦截);PASS/SKIP=0(SKIP=环境不支持,
533
- // ubuntu 测试门禁依赖 SKIP 正常退出;windows runner 上 koffi 随 optionalDependencies 必装)
534
- if (criteria.some((c) => c.status === 'FAIL')) process.exitCode = 1
535
- }
536
-
537
- main().catch((e) => { console.error('FATAL:', e instanceof Error ? `${e.message}\n${e.stack}` : e); process.exitCode = 1 })
@@ -1,7 +0,0 @@
1
- {
2
- "comment": "J-09 contract 基线:SessionEventMap/SessionEvent 签名字段集。只增不删——本文件中任何键从 src/session/log.ts 的当前签名中消失即 FAIL(M2部署设计增补 §2.3 J-09)",
3
- "eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork", "system"],
4
- "actors": ["user", "model", "plugin", "runtime"],
5
- "sessionEventFields": ["seq", "ts", "actor", "kind", "turnId", "payload", "meta", "prevHash", "hash"],
6
- "sessionLogApi": ["append", "snapshotModelRequest", "verifyChain", "deriveMessages", "assertModelVisibleLogged", "stats", "fromEvents", "exportJSONL"]
7
- }
@@ -1,5 +0,0 @@
1
- export const manifest = { name: 'hello-plugin', version: '1.0.0' }
2
- export default function apply(api: unknown) {
3
- const a = api as { registerTool: (t: unknown) => void }
4
- a.registerTool({ name: 'hello', run: async () => 'world' })
5
- }