-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathsandbox-architecture.html
More file actions
1308 lines (1143 loc) · 128 KB
/
Copy pathsandbox-architecture.html
File metadata and controls
1308 lines (1143 loc) · 128 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>沙箱:现状、八家对标与完整修复顺序</title>
<style>
:root{
--bg:#16150f; --pan:#1d1c14; --bd:#2e2c20; --tx:#bdb9a6; --acc:#d4843a;
--cb:#12110b; --dim:#8a8675;
--c0:#4f8ef7; --c1:#5aad4e; --c2:#d4843a; --c3:#9d6fe0; --c4:#2db3d5;
--no:#e0445a;
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--tx);
font:14px/1.7 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;padding:40px 24px 96px;
overflow-x:hidden}
.wrap{max-width:1180px;margin:0 auto}
h1{font-size:23px;color:#f0eee6;margin:0 0 6px}
.sub{color:var(--tx);margin:0 0 20px;max-width:80ch}
h2{font-size:16px;color:var(--acc);margin:34px 0 8px;scroll-margin-top:20px}
h2 .num{color:var(--dim);font-size:12px;margin-right:8px;letter-spacing:.16em}
.lead{color:var(--dim);font-size:13px;margin:0 0 12px;max-width:82ch}
.card{background:var(--pan);border:1px solid var(--bd);border-radius:12px;padding:8px;margin-bottom:16px}
svg{display:block;width:100%;height:auto;background:var(--cb);border-radius:8px}
.legend{font-size:12.5px;color:var(--dim);padding:8px 10px;line-height:1.8}
.legend b{color:#f0eee6}
code{background:var(--cb);color:var(--acc);padding:1px 5px;border-radius:4px;font:12px Menlo,monospace}
text{font-family:-apple-system,"Segoe UI",sans-serif}
/* 三层分层带 */
.band{margin:46px 0 6px;padding:12px 16px;border-radius:11px;border:1px solid var(--bd);
background:var(--pan);border-left-width:4px;scroll-margin-top:20px}
.band .lv{display:inline-block;font:10px Menlo,monospace;letter-spacing:.22em;
color:var(--cb);padding:3px 10px;border-radius:99px;margin-bottom:7px}
.band h2{margin:0 0 3px;font-size:18px;color:#f0eee6}
.band .w{color:var(--dim);font-size:12.5px;line-height:1.7;max-width:86ch}
.band.L1{border-left-color:var(--c0)} .band.L1 .lv{background:var(--c0)}
.band.L2{border-left-color:var(--c3)} .band.L2 .lv{background:var(--c3)}
.band.L3{border-left-color:var(--c1)} .band.L3 .lv{background:var(--c1)}
/* 目录 */
.tocg{margin:0 0 12px}
.tocg .gl{font:10px Menlo,monospace;letter-spacing:.2em;color:var(--dim);margin-bottom:6px}
.toc{display:flex;flex-wrap:wrap;gap:7px;margin:0}
.toc a{font-size:12px;color:var(--tx);text-decoration:none;border:1px solid var(--bd);
background:var(--pan);border-radius:99px;padding:4px 12px}
.toc a:hover{border-color:var(--acc);color:#f0eee6}
/* 平台双卡 */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
.plat{background:var(--pan);border:1px solid var(--bd);border-top-width:3px;border-radius:12px;padding:14px 16px}
.plat .k{font-size:10.5px;letter-spacing:.2em;color:var(--dim);margin-bottom:3px}
.plat h3{margin:0 0 3px;font-size:17px;font-weight:700}
.plat .noun{font:11.5px Menlo,monospace;color:var(--dim);margin-bottom:10px;word-break:break-all;line-height:1.7}
.plat .row{display:flex;gap:10px;padding:7px 0;border-top:1px solid #26241a;font-size:12.5px;line-height:1.65}
.plat .row .t{flex:0 0 78px;color:var(--dim);font-size:11.5px;padding-top:1px}
.plat .row .v{color:#a5a190}
.plat .row .v b{color:#f0eee6;font-weight:600}
.plat .row.bad .v b{color:var(--no)}
@media(max-width:900px){.grid2{grid-template-columns:1fr}}
/* 表格 */
.tbl{overflow-x:auto;background:var(--pan);border:1px solid var(--bd);border-radius:12px;margin-bottom:16px}
table{width:100%;border-collapse:collapse;font-size:13px;min-width:700px}
table.wide{min-width:1240px}
th{text-align:left;padding:10px 14px;color:var(--dim);font-weight:600;font-size:11px;
letter-spacing:.14em;border-bottom:1px solid var(--bd);white-space:nowrap}
td{padding:10px 14px;border-bottom:1px solid #26241a;vertical-align:top;line-height:1.65}
tr:last-child td{border-bottom:none}
td.y{color:var(--c1)} td.n{color:var(--no)} td.m{color:var(--c2)} td.g{color:#6f6b5c}
td b{color:#f0eee6;font-weight:600}
td.us{color:var(--no)}
th.us{color:var(--acc)}
.note{border-left:3px solid var(--bd);padding:2px 0 2px 13px;color:var(--dim);
font-size:12.5px;margin:0 0 16px;max-width:86ch;line-height:1.8}
.note b{color:#f0eee6}
.note.warn{border-left-color:var(--no)}
.note.warn b{color:var(--no)}
/* 修复路线 */
.steps{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}
.step{min-width:0;background:var(--pan);
border:1px solid var(--bd);border-radius:11px;padding:12px 14px;display:flex;flex-direction:column}
.step .n{font:10px Menlo,monospace;letter-spacing:.18em;color:var(--dim)}
.step h4{margin:5px 0 6px;font-size:14.5px;color:#f0eee6}
.step .d{font-size:12.5px;color:#a5a190;line-height:1.65;flex:1}
.step .x{margin-top:10px;padding-top:9px;border-top:1px dashed #34311f;font-size:12px;color:var(--dim);line-height:1.6}
.step .x b{color:var(--no);font-weight:600}
.step .src{margin-top:8px;font-size:11.5px;color:var(--c3);line-height:1.55}
.step.s1{border-top:3px solid var(--c2)}
.step.s2{border-top:3px solid var(--no)}
.step.s3{border-top:3px solid var(--c3)}
.step.s4{border-top:3px solid var(--c4)}
.step.s5{border-top:3px solid var(--c1)}
.step.s6{border-top:3px solid var(--c0)}
.step.s7{border-top:3px solid var(--c2)}
.step.s8{border-top:3px solid var(--c1)}
@media(max-width:900px){.steps{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:560px){.steps{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="wrap">
<h1>沙箱:当前实现、八家对标与实施记录</h1>
<p class="sub">这页分三层:<b>我们现在怎么做</b>(宿主原生边界、两个平台、持久开关、authority与25个执行面的当前分类)、
<b>别人怎么做</b>(<code>references/</code>下八个框架逐一,包括四个明确不做沙箱的)、
<b>我们采用了什么</b>(设计取舍、实施顺序、验收结果与已知限制)。
设计文档:<code>docs/reference/design/runtime/sandbox.md</code></p>
<div class="tocg">
<div class="gl">层一 · 我们现在怎么做</div>
<div class="toc">
<a href="#L1">总览</a>
<a href="#edge">01 一张边界图</a>
<a href="#plat">02 两个平台</a>
<a href="#switch">03 开关为什么失效</a>
<a href="#cover">04 授权与覆盖面</a>
</div>
</div>
<div class="tocg">
<div class="gl">层二 · 别人怎么做</div>
<div class="toc">
<a href="#L2">总览</a>
<a href="#spec">05 八家的隔离立场</a>
<a href="#mtx">06 四个方向逐家对照</a>
<a href="#best">07 独有机制与逐维度比较</a>
<a href="#cred">08 凭证这条为什么反过来</a>
</div>
</div>
<div class="tocg">
<div class="gl">层三 · 实施决策与记录</div>
<div class="toc">
<a href="#L3">总览</a>
<a href="#bridge">09 缺口、参考与采用结果</a>
<a href="#fix">10 修复路线</a>
<a href="#status">11 实现状态</a>
</div>
</div>
<!-- ══════════════════ 层一 ══════════════════ -->
<div class="band L1" id="L1">
<span class="lv">LAYER 1</span>
<h2>我们现在怎么做</h2>
<p class="w">系统调用级实现位于<code>openprogram/sandbox/__init__.py</code>,统一入口是<code>backend/local.py::_invocation</code>。
bash、process、execute_code、cron direct、memory写入器的MCP shell和one-shot MCP均复用该入口;文件工具使用同一份写入根与hard-floor校验。
下面四节记录四个方向的边界、两个平台各自的机制、开关为什么现在传得到,以及固定权限档常量表如何参与25个执行面的判定。
执行面计数是审计清单,不代表U01—U23都应采用同一种隔离策略。</p>
</div>
<div class="note"><b>已经确定的产品方向</b>:本地沙箱保持宿主原生实现,macOS使用Seatbelt,Linux使用bubblewrap,新安装默认<code>workspace-write</code>。
authority与pairing决定外部请求是否具有执行能力,沙箱限制已获准agent对宿主产生的副作用,并保留宿主真实开发环境。
Docker不是当前沙箱实现,也不是后端不可用时的自动回退;以后只有出现明确的独立Linux环境需求时,才设计为用户显式选择的第二后端。</div>
<h2 id="edge"><span class="num">01</span>一张边界图</h2>
<p class="lead">四个方向不对称:读允许访问大部分宿主文件但拒绝凭证清单,写限制到工作目录,执行不限制可执行文件但子进程继承沙箱,网络在两个平台都禁用。
凭证清单与环境变量白名单已经预置,但Linux不能实现中段glob匹配。</p>
<div class="card">
<svg viewBox="0 0 1160 700" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="沙箱四个方向的边界">
<defs>
<marker id="e-gn" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#5aad4e"/></marker>
<marker id="e-or" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#d4843a"/></marker>
<marker id="e-rd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#e0445a"/></marker>
</defs>
<text x="24" y="32" fill="#d4843a" font-size="16" font-weight="700">沙箱边界:四个方向</text>
<text x="24" y="55" fill="#8a8675" font-size="12.5">左边是受限子进程,中间竖线是沙箱边界,右边是宿主资源;图标表示每类访问的实际策略状态。</text>
<!-- 子进程 -->
<rect x="40" y="288" width="290" height="130" rx="12" fill="#131b2a" stroke="#4f8ef7" stroke-width="1.7"/>
<text x="185" y="322" fill="#4f8ef7" font-size="15.5" font-weight="700" text-anchor="middle">沙箱内的子进程</text>
<text x="185" y="348" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace" text-anchor="middle">bash -c <模型给的命令></text>
<text x="185" y="374" fill="#8a8675" font-size="11.5" text-anchor="middle">macOS由sandbox-exec包住</text>
<text x="185" y="394" fill="#8a8675" font-size="11.5" text-anchor="middle">Linux由bwrap包住</text>
<!-- 边界竖线 -->
<line x1="430" y1="96" x2="430" y2="646" stroke="#d4843a" stroke-width="1.5" stroke-dasharray="7 7" opacity=".8"/>
<text x="430" y="86" fill="#d4843a" font-size="12.5" text-anchor="middle" font-weight="700">沙箱边界</text>
<!-- 访问方向连线 -->
<path d="M330,340 C 372,340 384,168 424,168" fill="none" stroke="#d4843a" stroke-width="1.6"/>
<path d="M330,348 C 372,348 386,300 424,300" fill="none" stroke="#d4843a" stroke-width="1.6"/>
<path d="M330,358 C 372,358 386,420 424,420" fill="none" stroke="#5aad4e" stroke-width="1.6"/>
<path d="M330,366 C 372,366 384,556 424,556" fill="none" stroke="#e0445a" stroke-width="1.6"/>
<!-- 访问策略 01:读路径部分允许 -->
<circle cx="430" cy="168" r="19" fill="#241f10" stroke="#d4843a" stroke-width="1.8"/>
<path d="M422,159 L431,168 L422,177" fill="none" stroke="#d4843a" stroke-width="2"/>
<line x1="437" y1="156" x2="437" y2="180" stroke="#d4843a" stroke-width="2.2"/>
<line x1="452" y1="168" x2="530" y2="168" stroke="#d4843a" stroke-width="1.6" marker-end="url(#e-or)"/>
<!-- 访问策略 02:写路径部分允许 -->
<circle cx="430" cy="300" r="19" fill="#241f10" stroke="#d4843a" stroke-width="1.8"/>
<path d="M422,291 L431,300 L422,309" fill="none" stroke="#d4843a" stroke-width="2"/>
<line x1="437" y1="288" x2="437" y2="312" stroke="#d4843a" stroke-width="2.2"/>
<line x1="452" y1="300" x2="530" y2="300" stroke="#d4843a" stroke-width="1.6" marker-end="url(#e-or)"/>
<!-- 访问策略 03:执行允许 -->
<circle cx="430" cy="420" r="19" fill="#101a10" stroke="#5aad4e" stroke-width="1.8"/>
<path d="M422,411 L431,420 L422,429 M431,411 L440,420 L431,429" fill="none" stroke="#5aad4e" stroke-width="2"/>
<line x1="452" y1="420" x2="530" y2="420" stroke="#5aad4e" stroke-width="1.6" marker-end="url(#e-gn)"/>
<!-- 访问策略 04:网络拒绝 -->
<circle cx="430" cy="556" r="19" fill="#2a1215" stroke="#e0445a" stroke-width="1.8"/>
<path d="M422,548 L438,564 M438,548 L422,564" fill="none" stroke="#e0445a" stroke-width="2.2"/>
<line x1="452" y1="556" x2="512" y2="556" stroke="#e0445a" stroke-width="1.6" stroke-dasharray="5 5" marker-end="url(#e-rd)"/>
<!-- 目标 01 读 -->
<rect x="532" y="106" width="596" height="126" rx="10" fill="#241f10" stroke="#d4843a" stroke-width="1.5"/>
<text x="552" y="133" fill="#d4843a" font-size="14.5" font-weight="700">文件读 · 大部分宿主路径可读,凭证清单被拒绝</text>
<text x="552" y="156" fill="#8a8675" font-size="11.5" font-family="Menlo,monospace">macOS: allow file-read* 之后逐条deny正则 Linux: --perms 0000 --tmpfs / --ro-bind /dev/null</text>
<rect x="550" y="168" width="560" height="50" rx="8" fill="#101a10" stroke="#5aad4e" stroke-width="1.3"/>
<text x="566" y="188" fill="#5aad4e" font-size="12" font-weight="700">已验证的具体凭证路径不可读;macOS额外支持中段glob:</text>
<text x="566" y="208" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace">~/.ssh/** ~/.aws/** ~/.openprogram/auth/** ~/.claude.json keychain macOS:**/.env</text>
<!-- 目标 02 写 -->
<rect x="532" y="252" width="596" height="96" rx="10" fill="#241f10" stroke="#d4843a" stroke-width="1.5"/>
<text x="552" y="279" fill="#d4843a" font-size="14.5" font-weight="700">文件写 · 工作目录和临时目录,另有固定拒写路径</text>
<text x="552" y="302" fill="#a5a190" font-size="12">命令和文件工具都不能写入自动导入目录;已登记的owner安装来源只允许通过安装流程变更</text>
<text x="552" y="326" fill="#a5a190" font-size="12">macOS只允许当前进程<tspan font-family="Menlo,monospace" fill="#bdb9a6">TMPDIR</tspan>;Linux的<tspan font-family="Menlo,monospace" fill="#bdb9a6">/tmp</tspan>是隔离tmpfs,位于其下的工作目录由后续bind恢复</text>
<!-- 目标 03 执行 -->
<rect x="532" y="368" width="596" height="120" rx="10" fill="#101a10" stroke="#5aad4e" stroke-width="1.5"/>
<text x="552" y="395" fill="#5aad4e" font-size="14.5" font-weight="700">进程执行 · 两个平台都不限制,子进程继承沙箱</text>
<text x="552" y="419" fill="#a5a190" font-size="12">实测<tspan font-family="Menlo,monospace" fill="#bdb9a6">git</tspan>、<tspan font-family="Menlo,monospace" fill="#bdb9a6">python3</tspan>、<tspan font-family="Menlo,monospace" fill="#bdb9a6">make</tspan>、<tspan font-family="Menlo,monospace" fill="#bdb9a6">clang</tspan>、conda python、<tspan font-family="Menlo,monospace" fill="#bdb9a6">/usr/sbin</tspan>全部可跑</text>
<text x="552" y="441" fill="#a5a190" font-size="12">按路径限制exec挡不住执行:<tspan font-family="Menlo,monospace" fill="#bdb9a6">/bin/bash -c</tspan>自己就在里面,脚本可以从任何地方读进来</text>
<text x="552" y="466" fill="#8a8675" font-size="11.5">仍然不能执行<tspan font-family="Menlo,monospace" fill="#bdb9a6">ps</tspan>和<tspan font-family="Menlo,monospace" fill="#bdb9a6">top</tspan>:Seatbelt不允许在profile内exec setuid二进制</text>
<!-- 目标 04 网络 -->
<rect x="514" y="508" width="614" height="96" rx="10" fill="#2a1215" stroke="#e0445a" stroke-width="1.5" stroke-dasharray="6 5"/>
<text x="534" y="535" fill="#e0445a" font-size="14.5" font-weight="700">网络 · 出站入站全断,两个平台一致</text>
<text x="534" y="558" fill="#a5a190" font-size="12">macOS由<tspan font-family="Menlo,monospace" fill="#bdb9a6">(deny default)</tspan>拒绝网络;Linux使用<tspan font-family="Menlo,monospace" fill="#bdb9a6">--unshare-net</tspan></text>
<text x="534" y="582" fill="#8a8675" font-size="11.5">DNS、回环、Docker socket实测都不可访问;与openclaw启用容器后的默认网络状态相同</text>
<!-- 底部结论 -->
<rect x="40" y="628" width="1088" height="52" rx="9" fill="#1d1c14" stroke="#2e2c20"/>
<text x="60" y="652" fill="#f0eee6" font-size="13" font-weight="700">当前边界:</text>
<text x="144" y="652" fill="#bdb9a6" font-size="12.5">网络访问被拒绝;凭证清单阻止无网条件下把已知凭证复制到记忆库。</text>
<text x="144" y="672" fill="#8a8675" font-size="12">清单之外仍然允许读取宿主文件;该范围是当前产品选择,不是最小文件视图。</text>
</svg>
<div class="legend">
<b>图标说明</b>:双箭头=不限制,单箭头加竖线=部分允许,叉=拒绝。
连线颜色与策略状态一致,虚线表示请求被拒绝。
</div>
</div>
<!-- ══════════ 02 两个平台 ══════════ -->
<h2 id="plat"><span class="num">02</span>两个平台</h2>
<p class="lead">同一个<code>wrap_command</code>在两个平台走完全不同的机制,各自额外放行了一些东西,也各自有一处只在实跑里才会暴露的细节。</p>
<div class="grid2">
<div class="plat" style="border-top-color:#4f8ef7">
<div class="k">MACOS</div>
<h3 style="color:#4f8ef7">Seatbelt</h3>
<div class="noun">/usr/bin/sandbox-exec -p <profile> /bin/bash -c <命令></div>
<div class="row"><div class="t">机制</div><div class="v">profile内联生成,以<b>(deny default)</b>为默认策略,再增加具名允许规则。嵌套Seatbelt会被系统拒绝,内层不能放宽外层限制。</div></div>
<div class="row"><div class="t">额外放行</div><div class="v"><code>/dev/null</code>等五个字符设备用<b>require-all加vnode-type CHARACTER-DEVICE</b>放行读写。<b>sysctl-read</b>只允许<code>hw.*</code>和四个uname所需的<code>kern.*</code>名字;不再允许通用<b>mach-lookup</b>,剪贴板和Apple Events服务不可达。写范围只包含当前<b>TMPDIR</b>。</div></div>
<div class="row"><div class="t">屏蔽写法</div><div class="v">每条deny glob编译成锚定正则,<b>deny file-read*</b>和<b>deny file-write-unlink</b>成对发,被禁读的路径不能用删除反推存在性。工作目录拼进profile前先转义。</div></div>
<div class="row bad"><div class="t">正则限制</div><div class="v">Seatbelt的正则引擎<b>不支持</b><code>(?:…)</code>,使用非捕获分组会导致deny规则不生效且没有错误;引擎匹配解析符号链接后的真实路径,所以静态前缀是符号链接时要同时生成原路径和真实路径规则。</div></div>
</div>
<div class="plat" style="border-top-color:#5aad4e">
<div class="k">LINUX</div>
<h3 style="color:#5aad4e">bubblewrap</h3>
<div class="noun">bwrap --new-session --die-with-parent --unshare-pid --unshare-ipc --unshare-uts --cap-drop ALL --ro-bind / / --proc /proc --dev /dev --unshare-net --tmpfs /tmp --bind <cwd> <cwd></div>
<div class="row"><div class="t">机制</div><div class="v">挂载视图重建:根只读,工作目录单独bind成可写,net / pid / ipc / uts四个命名空间隔离,capability全丢。</div></div>
<div class="row"><div class="t">屏蔽写法</div><div class="v">目录用<code>--perms 0000 --tmpfs</code>遮蔽,文件用<code>--ro-bind /dev/null</code>替代。<code>--cap-drop ALL</code>移除<b>DAC_OVERRIDE</b>,所以容器内子进程是root时权限位仍然生效,实测返回<code>Permission denied</code>。</div></div>
<div class="row"><div class="t">进程隔离</div><div class="v"><code>--unshare-pid</code>之后实测:沙箱内只看得到4个PID,读宿主进程的<code>/proc/<pid>/environ</code>报文件不存在,<code>kill -9</code>报进程不存在,宿主进程照常活着。没有它的话,洗干净子进程环境也没用。</div></div>
<div class="row"><div class="t">可用性探测</div><div class="v">不只检查PATH。每个<code>bwrap</code>可执行文件首次使用前,以正式策略所需的mount、PID、IPC、UTS、network和capability限制执行<code>/bin/true</code>;已安装但被宿主禁止创建非特权user namespace时,按后端不可用处理。</div></div>
<div class="row bad"><div class="t">挂载顺序</div><div class="v"><code>--tmpfs /tmp</code>必须位于cwd bind<b>之前</b>,否则<code>/tmp</code>下的工作目录会被tmpfs遮蔽。拒绝访问的挂载还必须跳过宿主上不存在的路径:根是只读绑定,bubblewrap无法创建挂载点,强制挂载会使调用失败。两项均由运行测试确认。</div></div>
</div>
</div>
<div class="tbl">
<table>
<tr><th>维度</th><th>macOS</th><th>Linux</th></tr>
<tr><td><b>网络</b></td><td class="y">断(deny default兜住)</td><td class="y">断(--unshare-net)</td></tr>
<tr><td><b>整盘可读</b></td><td class="m">是,减去屏蔽清单</td><td class="m">是,减去屏蔽清单</td></tr>
<tr><td><b>屏蔽清单表达力</b></td><td class="y">正则,中间带通配符的glob也能表达</td><td class="m">只能盖路径,<code>**/.env</code>这类无对应实现,被丢弃</td></tr>
<tr><td><b>写范围</b></td><td class="y">cwd + 当前TMPDIR + /tmp + /private/tmp</td><td class="y">cwd + 隔离tmpfs /tmp</td></tr>
<tr><td><b>/tmp语义</b></td><td>真实/tmp,与宿主共享、可持久</td><td>空tmpfs,用完即弃</td></tr>
<tr><td><b>exec限制</b></td><td class="y">不限制,子进程继承profile</td><td class="y">不限制</td></tr>
<tr><td><b>/dev/null可写</b></td><td class="y">是</td><td class="y">是</td></tr>
<tr><td><b>看得到宿主进程</b></td><td class="y">否</td><td class="y">否,PID命名空间已隔离</td></tr>
<tr><td><b>环境变量</b></td><td class="y">白名单</td><td class="y">白名单</td></tr>
<tr><td><b>工作目录被遮蔽</b></td><td class="y">不会</td><td class="y">不会,tmpfs先挂</td></tr>
<tr><td><b>ps / top</b></td><td class="n">不能执行,setuid平台限制</td><td class="y">正常</td></tr>
<tr><td><b>用的shell</b></td><td>/bin/bash(不开沙箱时是/bin/sh)</td><td>/bin/bash</td></tr>
</table>
</div>
<!-- ══════════ 03 开关 ══════════ -->
<h2 id="switch"><span class="num">03</span>开关:载体从ContextVar换成配置</h2>
<p class="lead">开关原来是一个<code>ContextVar</code>:设它的地方和真正执行命令的地方不在同一个上下文里,值就丢了,三条路径里两条在丢,其中一条是用户最常用的Web。
下图是改之前的传递,留在这里是因为它说明了为什么载体必须换。</p>
<div class="card">
<svg viewBox="0 0 1160 470" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="开关在三条路径上的传递">
<defs>
<marker id="s-gy" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#8a8675"/></marker>
<marker id="s-gn" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#5aad4e"/></marker>
<marker id="s-rd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#e0445a"/></marker>
</defs>
<text x="24" y="30" fill="#d4843a" font-size="16" font-weight="700">改之前:开关从设置点到执行点</text>
<text x="24" y="52" fill="#8a8675" font-size="12.5">红色竖线是上下文断掉的地方。跨过它,新的执行环境拿到的是空Context,读回默认值false。</text>
<text x="46" y="80" fill="#8a8675" font-size="11" letter-spacing="1.5">① 在哪里设开关</text>
<text x="336" y="80" fill="#8a8675" font-size="11" letter-spacing="1.5">② 轮次实际在哪里跑</text>
<text x="626" y="80" fill="#8a8675" font-size="11" letter-spacing="1.5">③ 执行点读回什么</text>
<text x="916" y="80" fill="#8a8675" font-size="11" letter-spacing="1.5">④ 命令的实际状态</text>
<!-- Row 1 Web -->
<text x="24" y="140" fill="#e0445a" font-size="12.5" font-weight="700" transform="rotate(-90 24 140)" text-anchor="middle">WEB</text>
<rect x="40" y="110" width="252" height="86" rx="9" fill="#12110b" stroke="#2e2c20" stroke-width="1.3"/>
<text x="58" y="136" fill="#f0eee6" font-size="13" font-weight="700">websocket的asyncio任务</text>
<text x="58" y="158" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace">sandbox_enabled.set(True)</text>
<text x="58" y="180" fill="#5aad4e" font-size="11.5">界面立刻显示 Sandbox: ON</text>
<line x1="292" y1="153" x2="322" y2="153" stroke="#8a8675" stroke-width="1.5" marker-end="url(#s-gy)"/>
<line x1="312" y1="106" x2="312" y2="200" stroke="#e0445a" stroke-width="1.6" stroke-dasharray="5 5"/>
<text x="312" y="98" fill="#e0445a" font-size="11" text-anchor="middle">线程边界</text>
<rect x="330" y="110" width="252" height="86" rx="9" fill="#12110b" stroke="#e0445a" stroke-width="1.4"/>
<text x="348" y="136" fill="#f0eee6" font-size="13" font-weight="700">裸threading.Thread</text>
<text x="348" y="158" fill="#bdb9a6" font-size="11.5">新线程拿到的是空Context</text>
<text x="348" y="180" fill="#8a8675" font-size="11.5">整个webui目录没有copy_context</text>
<line x1="582" y1="153" x2="612" y2="153" stroke="#e0445a" stroke-width="1.5" marker-end="url(#s-rd)"/>
<rect x="620" y="110" width="252" height="86" rx="9" fill="#2a1215" stroke="#e0445a" stroke-width="1.4"/>
<text x="638" y="144" fill="#e0445a" font-size="15" font-weight="700">读回 false</text>
<text x="638" y="170" fill="#a5a190" font-size="11.5">和开关的显示状态相反</text>
<line x1="872" y1="153" x2="902" y2="153" stroke="#e0445a" stroke-width="1.5" marker-end="url(#s-rd)"/>
<rect x="910" y="110" width="216" height="86" rx="9" fill="#2a1215" stroke="#e0445a" stroke-width="1.4"/>
<text x="928" y="144" fill="#e0445a" font-size="14" font-weight="700">命令无沙箱执行</text>
<text x="928" y="170" fill="#a5a190" font-size="11.5">开关是个no-op</text>
<!-- Row 2 CLI -->
<text x="24" y="280" fill="#5aad4e" font-size="12.5" font-weight="700" transform="rotate(-90 24 280)" text-anchor="middle">CLI</text>
<rect x="40" y="250" width="252" height="86" rx="9" fill="#12110b" stroke="#2e2c20" stroke-width="1.3"/>
<text x="58" y="276" fill="#f0eee6" font-size="13" font-weight="700">REPL线程里的 /sandbox</text>
<text x="58" y="298" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace">sandbox_enabled.set(True)</text>
<text x="58" y="320" fill="#8a8675" font-size="11.5">同一个线程继续往下走</text>
<line x1="292" y1="293" x2="322" y2="293" stroke="#5aad4e" stroke-width="1.5" marker-end="url(#s-gn)"/>
<rect x="330" y="250" width="252" height="86" rx="9" fill="#101a10" stroke="#5aad4e" stroke-width="1.4"/>
<text x="348" y="284" fill="#f0eee6" font-size="13" font-weight="700">同一线程、同一Context</text>
<text x="348" y="310" fill="#8a8675" font-size="11.5">没有任何边界要跨</text>
<line x1="582" y1="293" x2="612" y2="293" stroke="#5aad4e" stroke-width="1.5" marker-end="url(#s-gn)"/>
<rect x="620" y="250" width="252" height="86" rx="9" fill="#101a10" stroke="#5aad4e" stroke-width="1.4"/>
<text x="638" y="284" fill="#5aad4e" font-size="15" font-weight="700">读回 true</text>
<text x="638" y="310" fill="#a5a190" font-size="11.5">开关在这条路径上有效</text>
<line x1="872" y1="293" x2="902" y2="293" stroke="#5aad4e" stroke-width="1.5" marker-end="url(#s-gn)"/>
<rect x="910" y="250" width="216" height="86" rx="9" fill="#101a10" stroke="#5aad4e" stroke-width="1.4"/>
<text x="928" y="284" fill="#5aad4e" font-size="14" font-weight="700">命令被包住</text>
<text x="928" y="310" fill="#a5a190" font-size="11.5">仅限同线程的bash</text>
<!-- Row 3 spawn -->
<text x="24" y="404" fill="#e0445a" font-size="12.5" font-weight="700" transform="rotate(-90 24 404)" text-anchor="middle">FUNC</text>
<rect x="40" y="374" width="252" height="80" rx="9" fill="#12110b" stroke="#2e2c20" stroke-width="1.3"/>
<text x="58" y="400" fill="#f0eee6" font-size="13" font-weight="700">父进程里开关为true</text>
<text x="58" y="424" fill="#8a8675" font-size="11.5">CLI或Web都可能是这个起点</text>
<line x1="292" y1="414" x2="322" y2="414" stroke="#8a8675" stroke-width="1.5" marker-end="url(#s-gy)"/>
<line x1="312" y1="370" x2="312" y2="458" stroke="#e0445a" stroke-width="1.6" stroke-dasharray="5 5"/>
<text x="312" y="362" fill="#e0445a" font-size="11" text-anchor="middle">进程边界</text>
<rect x="330" y="374" width="252" height="80" rx="9" fill="#12110b" stroke="#e0445a" stroke-width="1.4"/>
<text x="348" y="400" fill="#f0eee6" font-size="13" font-weight="700">mp spawn 出的新解释器</text>
<text x="348" y="424" fill="#8a8675" font-size="11.5">spawn不复制contextvars</text>
<line x1="582" y1="414" x2="612" y2="414" stroke="#e0445a" stroke-width="1.5" marker-end="url(#s-rd)"/>
<rect x="620" y="374" width="252" height="80" rx="9" fill="#2a1215" stroke="#e0445a" stroke-width="1.4"/>
<text x="638" y="404" fill="#e0445a" font-size="15" font-weight="700">读回 false</text>
<text x="638" y="430" fill="#a5a190" font-size="11.5">只有usage上下文被显式恢复了</text>
<line x1="872" y1="414" x2="902" y2="414" stroke="#e0445a" stroke-width="1.5" marker-end="url(#s-rd)"/>
<rect x="910" y="374" width="216" height="80" rx="9" fill="#2a1215" stroke="#e0445a" stroke-width="1.4"/>
<text x="928" y="404" fill="#e0445a" font-size="14" font-weight="700">全部无沙箱执行</text>
<text x="928" y="430" fill="#a5a190" font-size="11.5">每个agentic function里的bash</text>
</svg>
<div class="legend">
<b>三个边界的共同点</b>:每一个都在起一份新的执行环境。逐个补<code>copy_context()</code>并不等价:
spawn出来的子agent确实能靠它把开关带进worker线程,<b>但在followup线程上照样掉回默认值</b>,这是实测的;嵌套CLI根本不是线程,补不了。
一个要跨进程存活的用户级设置,本来就不该挂在上下文变量上。
</div>
</div>
<p class="lead">现在的载体是文件:策略在包装命令的那一刻从<code>~/.openprogram/config.json</code>的<code>sandbox.*</code>读出来。
文件不属于任何上下文,所以三个边界都跨得过去。</p>
<div class="card">
<div class="tbl">
<table>
<tr><th>路径</th><th>改之前</th><th>改之后(实测)</th></tr>
<tr><td><b>Web</b>:asyncio任务里设,普通线程里执行</td><td class="n">读回false,命令无沙箱执行</td><td class="y">线程内写工作目录之外被拒,<code>$OPENAI_API_KEY</code>长度为0</td></tr>
<tr><td><b>CLI</b>:REPL同线程</td><td class="y">有效</td><td class="y">有效,且跨会话保持</td></tr>
<tr><td><b>spawn</b>:<code>@agentic_function</code>的子解释器</td><td class="n">读回false,全部无沙箱执行</td><td class="y">子进程内写工作目录之外被拒,<code>$OPENAI_API_KEY</code>长度为0</td></tr>
<tr><td><b>审批bypass</b>:<code>permission_mode="bypass"</code></td><td class="n">短路到工具自己的execute</td><td class="y">照样被包住:策略在权限层之下解析,实测写越界和读SSH私钥都失败</td></tr>
<tr><td><b>平台后端不可用</b></td><td class="n">静默改为普通执行</td><td class="y">工具缺失或隔离机制探测失败时默认<code>refuse</code>并说明原因;<code>warn</code>可选</td></tr>
</table>
</div>
<div class="legend">
<b>七个配置键</b>:<code>sandbox.mode</code>、<code>writable_roots</code>、<code>deny_read</code>、<code>deny_write</code>、<code>network</code>、<code>pass_env</code>、<code>unavailable_policy</code>,
都在<code>config_schema.py</code>的<code>SETTINGS</code>里,所以<code>openprogram config</code>、setup向导、TUI设置页和Web设置页一起有。
新安装默认<code>mode=workspace-write</code>;已有用户的显式<code>danger-full-access</code>保持不变。平台工具不可用时默认拒绝,不退回宿主执行。
</div>
</div>
<!-- ══════════ 04 覆盖面 ══════════ -->
<h2 id="cover"><span class="num">04</span>authority tier与覆盖面</h2>
<p class="lead"><code>authority_tier</code>是请求边界上的<code>owner</code>/<code>paired</code>枚举;检查点用该枚举查询进程内写死的capability常量表。请求不能携带、创建或扩展capability列表。<code>principal</code>标识请求使用谁的授权以及谁能作出owner审批,不等于权限档。
permission rule或精确owner approval表达对本次操作的同意,<code>SandboxPolicy</code>限制操作实际可访问的宿主资源,不可取消的hard constraints独立于以上输入始终执行。字段定义与传播规则同时记录在<a href="../memory/speaker-identity.html#repair">speaker identity设计</a>中。</p>
<div class="card">
<svg viewBox="0 0 1160 236" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="authority tier、审批、沙箱和强制约束共同决定执行">
<defs>
<marker id="a-gy" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#6f6b5c"/></marker>
</defs>
<text x="24" y="30" fill="#d4843a" font-size="16" font-weight="700">一次宿主操作必须同时通过四项判定</text>
<rect x="24" y="58" width="244" height="88" rx="9" fill="#12110b" stroke="#4f8ef7" stroke-width="1.4"/>
<text x="42" y="82" fill="#4f8ef7" font-size="12.5" font-weight="700">TurnRequest运行时字段</text>
<text x="42" y="106" fill="#bdb9a6" font-size="11" font-family="Menlo,monospace">principal_id = owner</text>
<text x="42" y="126" fill="#bdb9a6" font-size="11" font-family="Menlo,monospace">authority_tier = owner | paired</text>
<line x1="270" y1="102" x2="302" y2="102" stroke="#6f6b5c" stroke-width="1.5" marker-end="url(#a-gy)"/>
<rect x="306" y="58" width="224" height="88" rx="9" fill="#12110b" stroke="#d4843a" stroke-width="1.4"/>
<text x="324" y="82" fill="#d4843a" font-size="12.5" font-weight="700">01 固定档位判定</text>
<text x="324" y="106" fill="#bdb9a6" font-size="11">operation映射到能力名</text>
<text x="324" y="126" fill="#e0445a" font-size="11">档位常量表不包含 → deny</text>
<line x1="532" y1="102" x2="564" y2="102" stroke="#6f6b5c" stroke-width="1.5" marker-end="url(#a-gy)"/>
<rect x="568" y="58" width="250" height="88" rx="9" fill="#12110b" stroke="#9d6fe0" stroke-width="1.4"/>
<text x="586" y="82" fill="#9d6fe0" font-size="12.5" font-weight="700">02 规则或精确owner审批</text>
<text x="586" y="106" fill="#bdb9a6" font-size="11">只有认证本地interactive可申请</text>
<text x="586" y="126" fill="#bdb9a6" font-size="11">cron / subagent / paired渠道不升级</text>
<line x1="820" y1="102" x2="852" y2="102" stroke="#6f6b5c" stroke-width="1.5" marker-end="url(#a-gy)"/>
<rect x="856" y="58" width="280" height="88" rx="9" fill="#101a10" stroke="#5aad4e" stroke-width="1.4"/>
<text x="874" y="82" fill="#5aad4e" font-size="12.5" font-weight="700">03 执行边界</text>
<text x="874" y="106" fill="#bdb9a6" font-size="11">SandboxPolicy / 外部容器 / 固定argv</text>
<text x="874" y="126" fill="#bdb9a6" font-size="11">所选边界必须能实施声明的限制</text>
<rect x="24" y="168" width="1112" height="44" rx="8" fill="#2a1418" stroke="#e0445a" stroke-width="1.4"/>
<text x="580" y="195" fill="#e0445a" font-size="12.5" font-weight="700" text-anchor="middle">04 hard constraints始终执行:任何权限档、审批、bypass、job authority或用户设置都不能取消</text>
</svg>
</div>
<div class="legend"><b>执行判定</b>:每次执行以及其他模型可影响的宿主副作用都必须计算
<code>allow = hard_constraints ∧ tier_capabilities[authority_tier].contains(capability) ∧ permission_or_exact_owner_approval ∧ enforcement_boundary</code>。
请求只携带权限档枚举;capability集合只存在于进程内常量表。模型正文、speaker和工具参数都不能创建或扩展它。</div>
<div class="tbl">
<table>
<tr><th>能力</th><th>允许的操作</th><th>默认归属</th></tr>
<tr><td><code>reply</code></td><td>向当前会话返回文本,不产生其他宿主副作用</td><td><code>owner</code>与<code>paired</code></td></tr>
<tr><td><code>memory.source.append</code></td><td>追加带speaker、<code>authority_tier</code>和信任状态的来源记录</td><td><code>owner</code>与<code>paired</code>;未配对消息不进入agent</td></tr>
<tr><td><code>memory.trusted.promote</code></td><td>把已审阅记录提升为owner轮次可自动召回的可信记忆</td><td>仅认证本地owner interactive</td></tr>
<tr><td><code>schedule.create</code><br><code>schedule.manage</code></td><td>创建映射到<code>schedule.create</code>;修改、重排或删除映射到<code>schedule.manage</code>。创建或修改获批后保存新的不可变job capability</td><td>仅认证本地owner interactive;属于宿主副作用</td></tr>
<tr><td><code>fs.read</code> / <code>fs.write</code><br><code>process.exec</code> / <code>network.send</code></td><td>读取或修改文件、启动进程、向外部系统发送</td><td>仅<code>owner</code>;仍需规则、审批和执行边界</td></tr>
</table>
</div>
<div class="tbl">
<table>
<tr><th>请求来源</th><th>principal</th><th>authority tier</th><th>缺失字段处理</th></tr>
<tr><td><b>认证本地Web / CLI / TUI</b></td><td><code>principal_id=owner</code><br><code>interaction=interactive</code></td><td><code>owner</code></td><td>入口必须主动构造;认证或owner配置无效时拒绝请求</td></tr>
<tr><td><b>已配对渠道</b></td><td>实例owner;稳定账号ID另存为speaker identity</td><td><code>paired</code></td><td>只按稳定账号ID匹配;显示名不参与身份判断</td></tr>
<tr><td><b>continuation / subagent</b></td><td>显式继承caller principal</td><td>显式继承同一档位;没有交互升级</td><td>序列化或恢复缺字段即状态错误并deny,不能依赖unknown fallback</td></tr>
<tr><td><b>cron触发</b></td><td>创建时批准的owner</td><td>不可变job authority中的<code>owner</code></td><td>触发时不得重算为interactive;字段或hash不完整即deny</td></tr>
<tr><td><b>未配对或无稳定ID的渠道消息</b></td><td>无agent principal</td><td>无</td><td>不进入agent;返回配对码。群组消息另存为pending证据,私聊不归档</td></tr>
</table>
</div>
<div class="note warn"><b>渠道记忆保留来源信任状态</b>:已配对渠道发言是可信来源,并可调用<code>memory.source.append</code>;<code>paired</code>档没有文件、进程、网络、调度、审批或runtime control能力。
未配对群组发言不进入agent,只归档为pending证据。pending证据当前仍可显式检索;hold队列准入和记忆读取过滤属于第二批,不能写成已实现。
只有认证本地owner能把已审阅记录执行<code>memory.trusted.promote</code>。由记忆内容引出的操作仍须通过当前轮次的权限档、权限规则、审批与沙箱检查,Writer和模型输出不能改变权限档或信任状态。</div>
<p class="lead">实施前审计把范围固定为25个执行面:当时2个已受管、23个未统一受管。当前U01—U23已经逐项采用强制策略或记录受信边界与保留理由;一个执行面按共同的命令来源与安全边界计数,不按每次<code>subprocess</code>调用计数。</p>
<div class="card">
<svg viewBox="0 0 1160 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="实施前25个执行面审计基线">
<text x="24" y="32" fill="#d4843a" font-size="16" font-weight="700">实施前基线:25个执行点,2个走沙箱</text>
<text x="24" y="55" fill="#8a8675" font-size="12.5">每个方块是一处会把内容变成进程的代码位置。</text>
<rect x="40" y="84" width="46" height="46" rx="7" fill="#101a10" stroke="#5aad4e" stroke-width="2"/>
<text x="63" y="113" fill="#5aad4e" font-size="18" font-weight="700" text-anchor="middle">✓</text>
<rect x="96" y="84" width="46" height="46" rx="7" fill="#101a10" stroke="#5aad4e" stroke-width="2"/>
<text x="119" y="113" fill="#5aad4e" font-size="18" font-weight="700" text-anchor="middle">✓</text>
<rect x="152" y="84" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="208" y="84" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="264" y="84" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="40" y="140" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="96" y="140" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="152" y="140" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="208" y="140" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="264" y="140" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="40" y="196" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="96" y="196" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="152" y="196" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="208" y="196" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="264" y="196" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="40" y="252" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="96" y="252" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="152" y="252" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="208" y="252" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="264" y="252" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="40" y="308" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="96" y="308" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="152" y="308" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="208" y="308" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<rect x="264" y="308" width="46" height="46" rx="7" fill="#16150f" stroke="#3a3729"/>
<text x="175" y="374" fill="#8a8675" font-size="11.5" text-anchor="middle">固定审计口径:25个执行面</text>
<rect x="356" y="84" width="772" height="60" rx="9" fill="#101a10" stroke="#5aad4e" stroke-width="1.4"/>
<text x="376" y="110" fill="#5aad4e" font-size="13.5" font-weight="700">实施前2处:bash / process → backend/local.py::_invocation,以及memory写入器MCP shell</text>
<text x="376" y="132" fill="#a5a190" font-size="12">前者还要满足:sandbox.mode打开、用local backend(docker和ssh后端不走)、平台工具在</text>
<rect x="356" y="156" width="772" height="198" rx="9" fill="#16150f" stroke="#3a3729" stroke-width="1.3"/>
<text x="376" y="182" fill="#f0eee6" font-size="13.5" font-weight="700">实施前其余23个执行面未统一受管;当前均已按下表分类和收口</text>
<text x="376" y="206" fill="#e0445a" font-size="12">P0模型来源:execute_code、cron direct/prompt、文件工具、watcher自动导入、嵌套CLI内置工具</text>
<text x="376" y="228" fill="#d4843a" font-size="12">受信配置:MCP stdio、shell hooks、plugins;需要安装与来源边界,不应按模型命令处理</text>
<text x="376" y="250" fill="#8a8675" font-size="12">第一方确定性执行与管理员CLI:逐项保留宿主权限并审计,不强制经过命令沙箱</text>
<text x="376" y="268" fill="#8a8675" font-size="11.5">修复目标是覆盖所有模型可影响的副作用,不是把每个subprocess调用统一包装</text>
<text x="376" y="300" fill="#f0eee6" font-size="12" font-weight="700">完成判据:下面U01—U23逐项标记信任类、强制策略与测试,不允许用“其他执行点”代替。</text>
<text x="376" y="324" fill="#8a8675" font-size="11.5">新增模型可影响的执行面必须先更新清单;没有归类或没有验证的条目阻止默认开启。</text>
</svg>
</div>
<div class="note"><b>实施前已受管的2个执行面</b>:M01是<code>bash</code>/<code>process</code>经local backend进入<code>backend/local.py::_invocation</code>;M02是memory写入器的MCP <code>shell</code>使用同一份<code>SandboxPolicy</code>。下面记录U01—U23的闭合方式。</div>
<div class="tbl">
<table>
<tr><th>编号</th><th>实施前未统一受管的执行面</th><th>来源类</th><th>已实施边界 / 明确保留理由</th></tr>
<tr><td><b>U01</b></td><td><code>backend/docker.py</code></td><td>模型命令 / 外部容器</td><td>声明容器是执行边界;命令仍经过工具规则、审批与审计,不宣称使用宿主sandbox</td></tr>
<tr><td><b>U02</b></td><td><code>backend/ssh.py</code></td><td>模型命令 / 远端主机</td><td>声明远端主机是执行边界;目标由owner配置,命令仍经过工具规则、审批与审计</td></tr>
<tr><td><b>U03</b></td><td><code>programs/functions/execute_code</code></td><td class="n">模型代码</td><td>local路径通过<code>LocalBackend.run</code>复用<code>_invocation</code>;解释器、cwd与临时脚本均进入同一策略测试</td></tr>
<tr><td><b>U04</b></td><td><code>cron/worker.py</code> direct command</td><td class="n">模型请求保存 / 无人值守</td><td>创建先检查<code>schedule.create</code>,修改、重排或删除先检查<code>schedule.manage</code>;<code>paired</code>档必须在任何job持久化变更前deny。认证本地owner批准创建或修改后保存新的不可变job authority,固化principal、<code>authority_tier=owner</code>、command、cwd、policy与hash;触发时强制沙箱,ask与升级均deny</td></tr>
<tr><td><b>U05</b></td><td><code>cron/worker.py</code> prompt job</td><td class="n">模型prompt / 无人值守</td><td>同样要求owner批准创建;重建请求时显式<code>source=cron</code>并加载不可变job authority,内部所有副作用继续按<code>owner</code>档常量表检查</td></tr>
<tr><td><b>U06</b></td><td><code>memory/agent_runtime/claude_code.py</code></td><td class="n">模型驱动的嵌套agent</td><td>协议层关闭内置命令与文件工具,注入OpenProgram受管MCP工具</td></tr>
<tr><td><b>U07</b></td><td><code>functions/watcher.py → _registry.exec_module</code></td><td class="n">模型可写Python</td><td>任何模型可写目录禁止自动导入;导入目录改为owner控制并记录来源</td></tr>
<tr><td><b>U08</b></td><td><code>webui/_functions.py</code>动态重载</td><td class="n">模型可写Python</td><td>与U07共享同一条目录所有权约束;不能在宿主进程重载模型写入的模块</td></tr>
<tr><td><b>U09</b></td><td><code>agent/process_runner.py</code></td><td>已注册agentic function</td><td>spawn请求显式序列化SandboxPolicy;子进程内的命令、文件与嵌套工具保持同一策略</td></tr>
<tr><td><b>U10</b></td><td><code>plugins/loader.py</code></td><td>owner安装的plugin</td><td>plugin清单与代码目录不可由模型修改;加载事件记录来源。trust默认<code>untrusted</code>且拒绝加载,只有owner显式提升信任的plugin才加载,并在宿主进程内执行(<code>plugins/sandbox.py</code>的subprocess隔离未实现,不宣称已进程隔离)</td></tr>
<tr><td><b>U11</b></td><td><code>mcp/client.py</code> stdio server</td><td>owner配置的server</td><td>MCP配置不可由模型静默改写;启动argv与环境显式保存并审计</td></tr>
<tr><td><b>U12</b></td><td><code>webui/routes/mcp.py</code> one-shot</td><td class="n">HTTP请求体</td><td>要求已认证owner请求并进入与MCP stdio一致的启动策略;删除“已沙箱”但实现未隔离的声明</td></tr>
<tr><td><b>U13</b></td><td><code>events/shell_hooks.py</code> gate command</td><td>owner配置;模型事件作stdin</td><td>命令本身按受信配置处理;配置目录受保护,超时或启动失败不能成为安全强制规则</td></tr>
<tr><td><b>U14</b></td><td><code>events/shell_hooks.py</code> notifier command</td><td>owner配置;模型事件作stdin</td><td>保持通知用途并审计;不得把notifier结果用于允许高风险操作</td></tr>
<tr><td><b>U15</b></td><td><code>providers/_shared/cli_backend/runner.py</code></td><td>模型prompt / 外部CLI</td><td>若接入生产,先声明CLI内置工具策略;保持未引用时不得计为现有保护</td></tr>
<tr><td><b>U16</b></td><td><code>programs/functions/grep</code></td><td>模型参数 / 固定argv</td><td>保持<code>shell=False</code>与<code>--</code>分隔;增加路径范围测试,不需要命令sandbox</td></tr>
<tr><td><b>U17</b></td><td><code>programs/functions/worktree</code></td><td>模型参数 / 固定git argv</td><td>仅允许受管仓库与固定子命令;保持argv调用并测试路径规范化</td></tr>
<tr><td><b>U18</b></td><td><code>programs/functions/agent_browser</code></td><td>模型参数 / 浏览器进程</td><td>浏览器profile与下载路径使用独立策略;启动参数保持argv并进入工具审批</td></tr>
<tr><td><b>U19</b></td><td>session/project/shadow store的git plumbing</td><td>第一方确定性argv</td><td>保留宿主执行;不接受模型shell字符串,测试参数分隔和仓库范围</td></tr>
<tr><td><b>U20</b></td><td><code>cli/commands/programs.py</code></td><td>owner主动安装 / 更新</td><td>保留管理员CLI权限;与agent工具不可互调,记录安装来源</td></tr>
<tr><td><b>U21</b></td><td><code>cli/commands/browser.py</code></td><td>owner主动安装</td><td>保留管理员CLI权限;安装命令不暴露给模型</td></tr>
<tr><td><b>U22</b></td><td><code>cli/commands/upgrade.py</code>与updater</td><td>owner主动升级</td><td>保留管理员CLI权限;下载、替换和重启必须来自显式升级操作</td></tr>
<tr><td><b>U23</b></td><td><code>plugins/installer.py</code></td><td>owner主动安装</td><td>保留管理员CLI权限;安装入口不暴露给模型,安装后由U10约束加载</td></tr>
</table>
</div>
<div class="legend"><b>清单结果</b>:U03—U09与U12的模型影响面已处理;U04/U05的测试证明paired渠道不能创建、修改、重排或删除job,owner批准生成的job capability在触发时不能扩大。
U01—U02明确使用外部边界并保留审批;U10—U11、U13—U15固定owner配置边界;U16—U23通过固定argv或管理员入口及回归测试保留宿主权限。任何新增或重新接入的执行面必须先加入此表,再决定是否使用OS沙箱。</div>
<p class="lead">下面保留修复前的自动导入流程。它说明为什么只修改<code>wrap_command</code>不够:直接文件工具和进程内导入需要各自的强制边界。当前实现已经在文件工具写入检查和owner登记来源两处阻止该流程。</p>
<div class="card">
<svg viewBox="0 0 1160 232" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="修复前模型输出到宿主代码的未受限执行路径">
<defs>
<marker id="p-rd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#e0445a"/></marker>
</defs>
<text x="24" y="32" fill="#e0445a" font-size="15.5" font-weight="700">实施前路径:模型输出 → 宿主进程内代码</text>
<rect x="24" y="58" width="248" height="92" rx="10" fill="#12110b" stroke="#e0445a" stroke-width="1.4"/>
<text x="42" y="84" fill="#f0eee6" font-size="13" font-weight="700">01 模型调 write</text>
<text x="42" y="107" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace">programs/functions/agentic/x.py</text>
<text x="42" y="130" fill="#8a8675" font-size="11.5">写文件这个动作本身不可疑</text>
<line x1="272" y1="104" x2="300" y2="104" stroke="#e0445a" stroke-width="1.6" marker-end="url(#p-rd)"/>
<rect x="308" y="58" width="248" height="92" rx="10" fill="#12110b" stroke="#e0445a" stroke-width="1.4"/>
<text x="326" y="84" fill="#f0eee6" font-size="13" font-weight="700">02 watcher 轮询</text>
<text x="326" y="107" fill="#bdb9a6" font-size="11.5">每2秒扫一次该目录</text>
<text x="326" y="130" fill="#8a8675" font-size="11.5">变化触发 _registry.rescan()</text>
<line x1="556" y1="104" x2="584" y2="104" stroke="#e0445a" stroke-width="1.6" marker-end="url(#p-rd)"/>
<rect x="592" y="58" width="248" height="92" rx="10" fill="#12110b" stroke="#e0445a" stroke-width="1.4"/>
<text x="610" y="84" fill="#f0eee6" font-size="13" font-weight="700">03 exec_module</text>
<text x="610" y="107" fill="#bdb9a6" font-size="11.5" font-family="Menlo,monospace">spec.loader.exec_module()</text>
<text x="610" y="130" fill="#8a8675" font-size="11.5">导入即执行模块级代码</text>
<line x1="840" y1="104" x2="868" y2="104" stroke="#e0445a" stroke-width="1.6" marker-end="url(#p-rd)"/>
<rect x="876" y="58" width="252" height="92" rx="10" fill="#2a1215" stroke="#e0445a" stroke-width="1.6"/>
<text x="894" y="84" fill="#e0445a" font-size="13" font-weight="700">04 在agent进程内运行</text>
<text x="894" y="107" fill="#a5a190" font-size="11.5">拿到宿主进程的全部权限</text>
<text x="894" y="130" fill="#a5a190" font-size="11.5">包括进程内存里的凭证</text>
<rect x="24" y="164" width="1104" height="46" rx="8" fill="#16150f" stroke="#3a3729"/>
<text x="44" y="192" fill="#8a8675" font-size="12.5">实施前不经工具审批或shell。当前<tspan font-family="Menlo,monospace">write/edit/apply_patch</tspan>先执行写入根和hard-floor检查,watcher只导入owner登记来源;任一条件不满足都不能进入<tspan font-family="Menlo,monospace">exec_module</tspan>。</text>
</svg>
</div>
<div class="note warn"><b>实施前的subagent bypass缺口已经关闭</b>:旧实现让<code>permission_mode="bypass"</code>在自动危险工具检查之前返回,影响
<code>bash</code>、<code>execute_code</code>、<code>process</code>以及<code>write</code>/<code>edit</code>/<code>apply_patch</code>。
当前hard constraints与capability检查位于bypass之前;命令使用宿主原生沙箱,文件工具执行统一路径检查,subagent没有交互升级能力。</div>
<div class="tbl">
<table>
<tr><th>实施前P0缺口</th><th>实施前源码行为</th><th>影响</th><th>当前闭合方式</th></tr>
<tr><td><b>“Always allow”过宽</b></td><td>审批持久化时只保存工具名;一次bash批准会得到<code>allow bash</code></td><td class="n">后续任意bash都可跳过审批,范围大于原命令</td><td class="y">已完成:只保存规范化后的精确操作;复杂shell只允许单次批准</td></tr>
<tr><td><b>文件工具与watcher组合</b></td><td>文件工具可写工作区外;watcher对<code>programs/functions/agentic/</code>变化调用<code>exec_module</code></td><td class="n">模型可写Python并由宿主进程自动导入</td><td class="y">已完成:文件工具统一写入根检查;自动导入只接受owner登记来源</td></tr>
<tr><td><b>cron无人值守执行</b></td><td>direct job使用<code>Popen(..., shell=True)</code>;prompt job启动分离CLI</td><td class="n">触发时没有交互审批路径,也没有统一沙箱</td><td class="y">已完成:签名执行spec、固化owner档、强制沙箱、ask与升级均deny</td></tr>
<tr><td><b>execute_code本地执行</b></td><td>模型给代码和解释器路径,随后<code>subprocess.run</code></td><td class="n">不经过<code>Backend._invocation</code></td><td class="y">已完成:复用local backend及同一份SandboxPolicy</td></tr>
<tr><td><b>命令规则按原字符串前缀匹配</b></td><td>未先处理ANSI、NUL、Unicode NFKC或<code>env X=1</code>包装</td><td class="n">保存规则与实际argv不一致;复杂shell文本也不能靠前缀安全解析</td><td class="y">已完成:规范化、shell解析、透明env包装与精确规则</td></tr>
<tr><td><b>runtime sandbox参数被忽略</b></td><td><code>create_runtime(sandbox="read-only")</code>的provider参数接受但不执行</td><td class="n">调用方看到的配置不构成隔离</td><td class="y">已完成:进程边界显式序列化并安装SandboxPolicy snapshot</td></tr>
</table>
</div>
<!-- ══════════════════ 层二 ══════════════════ -->
<div class="band L2" id="L2">
<span class="lv">LAYER 2</span>
<h2>别人怎么做</h2>
<p class="w"><code>references/</code>下八个框架逐一:<code>claude-code</code>、<code>codex-cli</code>、<code>openclaw</code>、<code>opencode</code>、
<code>hermes-agent</code>、<code>pi-mono</code>、<code>pi-ai</code>、<code>weclaw</code>。
其中四个完全不做系统级沙箱,这本身是一种设计立场,下面写清它们各自靠什么替代。
计数上有两点:<code>pi-ai</code>是<code>pi-mono</code>同上游的只读子集、没有执行面;
<code>claude-code</code>和<code>pi-mono</code>的扩展调的是同一个外部包,所以这一组里独立的系统调用级实现只有两份。</p>
</div>
<h2 id="spec"><span class="num">05</span>八家的隔离立场</h2>
<p class="lead">按隔离强度分四档。左边是主动移除隔离的,右边是把执行推到另一个内核视图或另一台机器的。
同一个框架出现两次,表示它跨档:左边是默认路径,右边是可选配置。</p>
<div class="card">
<svg viewBox="0 0 1160 580" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="八个框架按隔离强度分档">
<defs>
<linearGradient id="grad-str" x1="0" y1="0" x2="1" y2="0">
<stop offset="0%" stop-color="#e0445a"/><stop offset="38%" stop-color="#d4843a"/>
<stop offset="72%" stop-color="#5aad4e"/><stop offset="100%" stop-color="#2db3d5"/>
</linearGradient>
</defs>
<text x="24" y="30" fill="#d4843a" font-size="16" font-weight="700">隔离强度四档</text>
<text x="24" y="52" fill="#8a8675" font-size="12.5">档位说的是"模型给的命令最终在什么边界里执行",不是框架的成熟度。</text>
<rect x="24" y="66" width="1112" height="6" rx="3" fill="url(#grad-str)" opacity=".75"/>
<text x="24" y="88" fill="#8a8675" font-size="11">弱</text>
<text x="1136" y="88" fill="#8a8675" font-size="11" text-anchor="end">强</text>
<!-- Zone 0 -->
<rect x="24" y="100" width="266" height="392" rx="11" fill="#1c1116" stroke="#e0445a" stroke-width="1.4"/>
<text x="40" y="126" fill="#e0445a" font-size="11" font-family="Menlo,monospace" letter-spacing="2">档 00</text>
<text x="40" y="148" fill="#f0eee6" font-size="14.5" font-weight="700">移除隔离</text>
<text x="40" y="168" fill="#8a8675" font-size="11.5">把被包的agent的沙箱关掉</text>
<rect x="36" y="182" width="242" height="70" rx="8" fill="#12110b" stroke="#e0445a" stroke-width="1.3"/>
<text x="50" y="204" fill="#e0445a" font-size="13.5" font-weight="700">weclaw</text>
<text x="50" y="224" fill="#bdb9a6" font-size="11" font-family="Menlo,monospace">sandbox: danger-full-access</text>
<text x="50" y="242" fill="#8a8675" font-size="11.5">审批请求一律自动答allow</text>
<!-- Zone 1 -->
<rect x="306" y="100" width="266" height="392" rx="11" fill="#1e1a12" stroke="#d4843a" stroke-width="1.4"/>
<text x="322" y="126" fill="#d4843a" font-size="11" font-family="Menlo,monospace" letter-spacing="2">档 01</text>
<text x="322" y="148" fill="#f0eee6" font-size="14.5" font-weight="700">只有应用层策略</text>
<text x="322" y="168" fill="#8a8675" font-size="11.5">规则、审批、命令模式匹配</text>
<rect x="318" y="182" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="332" y="204" fill="#f0eee6" font-size="13.5" font-weight="700">opencode</text>
<text x="332" y="224" fill="#a5a190" font-size="11.5">SECURITY.md明确列为非目标</text>
<text x="332" y="242" fill="#8a8675" font-size="11.5">allow / ask / deny 三效果规则表</text>
<rect x="318" y="264" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="332" y="286" fill="#f0eee6" font-size="13.5" font-weight="700">pi-mono 核心</text>
<text x="332" y="306" fill="#bdb9a6" font-size="11" font-family="Menlo,monospace">spawn(bash, -c, cmd) 带全环境</text>
<text x="332" y="324" fill="#8a8675" font-size="11.5">没有审批,也没有 --yolo 可绕</text>
<rect x="318" y="346" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="332" y="368" fill="#f0eee6" font-size="13.5" font-weight="700">hermes-agent 默认</text>
<text x="332" y="388" fill="#bdb9a6" font-size="11" font-family="Menlo,monospace">TERMINAL_ENV=local,宿主bash</text>
<text x="332" y="406" fill="#8a8675" font-size="11.5">三层命令守卫顶上,见第07节</text>
<!-- Zone 2 -->
<rect x="588" y="100" width="266" height="392" rx="11" fill="#131b13" stroke="#5aad4e" stroke-width="1.4"/>
<text x="604" y="126" fill="#5aad4e" font-size="11" font-family="Menlo,monospace" letter-spacing="2">档 02</text>
<text x="604" y="148" fill="#f0eee6" font-size="14.5" font-weight="700">系统调用级沙箱</text>
<text x="604" y="168" fill="#8a8675" font-size="11.5">Seatbelt / bubblewrap / seccomp</text>
<rect x="600" y="182" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="614" y="204" fill="#f0eee6" font-size="13.5" font-weight="700">claude-code</text>
<text x="614" y="224" fill="#a5a190" font-size="11.5">实现在外部包 sandbox-runtime</text>
<text x="614" y="242" fill="#8a8675" font-size="11.5">网络走逐域名弹窗的代理</text>
<rect x="600" y="264" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="614" y="286" fill="#f0eee6" font-size="13.5" font-weight="700">codex-cli</text>
<text x="614" y="306" fill="#a5a190" font-size="11.5">仓库内自研,四档沙箱模式</text>
<text x="614" y="324" fill="#5aad4e" font-size="11.5">这一组里唯一的in-tree实现</text>
<rect x="600" y="346" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="614" y="368" fill="#f0eee6" font-size="13.5" font-weight="700">pi-mono 加扩展</text>
<text x="614" y="388" fill="#a5a190" font-size="11.5">调的是同一个外部包</text>
<text x="614" y="406" fill="#8a8675" font-size="11.5">通过整个换掉bash工具接进来</text>
<rect x="600" y="428" width="242" height="52" rx="8" fill="#241f10" stroke="#d4843a" stroke-width="1.8"/>
<text x="614" y="450" fill="#d4843a" font-size="13.5" font-weight="700">OpenProgram</text>
<text x="614" y="470" fill="#a5a190" font-size="11.5">Seatbelt / bubblewrap,默认workspace-write</text>
<!-- Zone 3 -->
<rect x="870" y="100" width="266" height="392" rx="11" fill="#111d21" stroke="#2db3d5" stroke-width="1.4"/>
<text x="886" y="126" fill="#2db3d5" font-size="11" font-family="Menlo,monospace" letter-spacing="2">档 03</text>
<text x="886" y="148" fill="#f0eee6" font-size="14.5" font-weight="700">容器或远端</text>
<text x="886" y="168" fill="#8a8675" font-size="11.5">换一个内核视图或换一台机器</text>
<rect x="882" y="182" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="896" y="204" fill="#f0eee6" font-size="13.5" font-weight="700">openclaw</text>
<text x="896" y="224" fill="#a5a190" font-size="11.5">默认off;Docker为默认sandbox backend</text>
<text x="896" y="242" fill="#8a8675" font-size="11.5">无网、只读根、cap-drop ALL</text>
<rect x="882" y="264" width="242" height="70" rx="8" fill="#12110b" stroke="#3a3729" stroke-width="1.3"/>
<text x="896" y="286" fill="#f0eee6" font-size="13.5" font-weight="700">hermes-agent 可选后端</text>
<text x="896" y="306" fill="#a5a190" font-size="11.5">docker / modal / daytona 等五种</text>
<text x="896" y="324" fill="#8a8675" font-size="11.5">容器内整个跳过守卫层</text>
<!-- 注 -->
<rect x="24" y="508" width="1112" height="58" rx="9" fill="#16150f" stroke="#3a3729"/>
<text x="44" y="532" fill="#8a8675" font-size="12"><tspan fill="#f0eee6" font-weight="700">pi-ai不在图上</tspan>:14个文件、没有工具层、没有spawn,没有可隔离的东西,列在文字里只为把八家覆盖完整。</text>
<text x="44" y="554" fill="#8a8675" font-size="12">四个不做系统级沙箱的是<tspan fill="#bdb9a6">opencode</tspan>、<tspan fill="#bdb9a6">pi-mono核心</tspan>、<tspan fill="#bdb9a6">hermes-agent默认路径</tspan>、<tspan fill="#bdb9a6">weclaw</tspan>。隔离强度跟着执行地点走,不跟着框架成熟度走。</text>
</svg>
</div>
<div class="note"><b>openclaw需要按配置状态与后端分别读取</b>:出厂<code>sandbox.mode=off</code>,关闭时审批默认放行;
启用且选择<code>backend=docker</code>时创建Docker容器,默认<code>network=none</code>、root filesystem只读、<code>cap-drop ALL</code>并设置<code>no-new-privileges</code>。
它也支持SSH与插件提供的OpenShell后端,因此Docker配置不能用于描述所有后端,更不能用于描述其出厂执行状态。</div>
<h2 id="mtx"><span class="num">06</span>四个方向逐家对照</h2>
<p class="lead">同样四个方向,加上一列子进程环境变量,因为凭证从环境里漏出去和从磁盘上漏出去是两条独立的路。
绿=收住,橙=部分,红=敞开,灰=不适用。</p>
<div class="card">
<svg viewBox="0 0 1160 620" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="八个框架在四个方向加环境变量上的对照矩阵">
<text x="12" y="30" fill="#d4843a" font-size="16" font-weight="700">读 / 写 / 执行 / 网络 / 子进程环境变量</text>
<text x="12" y="52" fill="#8a8675" font-size="12.5">每格是这一家在这个方向上的实际状态,取自实现而不是文档描述。</text>
<text x="20" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4">框架</text>
<text x="294" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4" text-anchor="middle">读</text>
<text x="482" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4" text-anchor="middle">写</text>
<text x="670" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4" text-anchor="middle">执行</text>
<text x="858" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4" text-anchor="middle">网络</text>
<text x="1046" y="90" fill="#8a8675" font-size="11" letter-spacing="1.4" text-anchor="middle">子进程环境变量</text>
<line x1="12" y1="98" x2="1148" y2="98" stroke="#2e2c20" stroke-width="1"/>
<!-- row 1 claude-code -->
<text x="20" y="127" fill="#f0eee6" font-size="12.5" font-weight="600">claude-code</text>
<rect x="203" y="106" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="213" y="127" fill="#d4843a" font-size="11">整盘,deny列表为空</text>
<rect x="391" y="106" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="401" y="127" fill="#5aad4e" font-size="11">默认拒加白名单</text>
<rect x="579" y="106" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="589" y="127" fill="#5aad4e" font-size="11">不限,子进程继承</text>
<rect x="767" y="106" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="777" y="127" fill="#5aad4e" font-size="11">代理,逐域名弹窗</text>
<rect x="955" y="106" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="965" y="127" fill="#d4843a" font-size="11">由runtime处理</text>
<!-- row 2 codex-cli -->
<text x="20" y="171" fill="#f0eee6" font-size="12.5" font-weight="600">codex-cli</text>
<rect x="203" y="150" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="213" y="171" fill="#d4843a" font-size="11">整盘,引擎有但清单为空</text>
<rect x="391" y="150" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="401" y="171" fill="#5aad4e" font-size="11">默认拒,护住 .git</text>
<rect x="579" y="150" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="589" y="171" fill="#5aad4e" font-size="11">不限</text>
<rect x="767" y="150" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="777" y="171" fill="#5aad4e" font-size="11">默认断,可配白名单</text>
<rect x="955" y="150" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="965" y="171" fill="#d4843a" font-size="11">可配,默认不过滤</text>
<!-- row 3 openclaw -->
<text x="20" y="215" fill="#f0eee6" font-size="12.5" font-weight="600">openclaw(启用时)</text>
<rect x="203" y="194" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="213" y="215" fill="#5aad4e" font-size="11">容器内只有两个挂载</text>
<rect x="391" y="194" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="401" y="215" fill="#5aad4e" font-size="11">工作区挂载默认只读</text>
<rect x="579" y="194" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="589" y="215" fill="#5aad4e" font-size="11">不限,argv先解包</text>
<rect x="767" y="194" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="777" y="215" fill="#5aad4e" font-size="11">network none,host被拦</text>
<rect x="955" y="194" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="965" y="215" fill="#5aad4e" font-size="11">名字正则加取值启发式</text>
<!-- row 4 pi-mono core -->
<text x="20" y="259" fill="#f0eee6" font-size="12.5" font-weight="600">pi-mono 核心</text>
<rect x="203" y="238" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="213" y="259" fill="#e0445a" font-size="11">不限,无工作区约束</text>
<rect x="391" y="238" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="401" y="259" fill="#e0445a" font-size="11">不限</text>
<rect x="579" y="238" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="589" y="259" fill="#e0445a" font-size="11">不限,没有审批</text>
<rect x="767" y="238" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="777" y="259" fill="#e0445a" font-size="11">不限</text>
<rect x="955" y="238" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="965" y="259" fill="#e0445a" font-size="11">全量继承</text>
<!-- row 5 pi-mono ext -->
<text x="20" y="303" fill="#f0eee6" font-size="12.5" font-weight="600">pi-mono 加扩展</text>
<rect x="203" y="282" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="213" y="303" fill="#5aad4e" font-size="11">denyRead 已预置</text>
<rect x="391" y="282" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="401" y="303" fill="#5aad4e" font-size="11">只准 cwd 和 /tmp</text>
<rect x="579" y="282" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="589" y="303" fill="#d4843a" font-size="11">不限</text>
<rect x="767" y="282" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="777" y="303" fill="#5aad4e" font-size="11">10个域名的白名单</text>
<rect x="955" y="282" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="965" y="303" fill="#e0445a" font-size="11">全量继承,扩展没传env</text>
<!-- row 6 hermes -->
<text x="20" y="347" fill="#f0eee6" font-size="12.5" font-weight="600">hermes-agent</text>
<rect x="203" y="326" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="213" y="347" fill="#d4843a" font-size="11">不限,deny自称非边界</text>
<rect x="391" y="326" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="401" y="347" fill="#d4843a" font-size="11">敏感系统路径拒写</text>
<rect x="579" y="326" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="589" y="347" fill="#d4843a" font-size="11">47条正则加hardline</text>
<rect x="767" y="326" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="777" y="347" fill="#e0445a" font-size="11">有开关但传不进去</text>
<rect x="955" y="326" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e"/><text x="965" y="347" fill="#5aad4e" font-size="11">注册表推导后剥离</text>
<!-- row 7 opencode -->
<text x="20" y="391" fill="#f0eee6" font-size="12.5" font-weight="600">opencode</text>
<rect x="203" y="370" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="213" y="391" fill="#d4843a" font-size="11">*.env 走 ask</text>
<rect x="391" y="370" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="401" y="391" fill="#d4843a" font-size="11">只有规则,无隔离</text>
<rect x="579" y="370" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a"/><text x="589" y="391" fill="#d4843a" font-size="11">只有规则,无隔离</text>
<rect x="767" y="370" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="777" y="391" fill="#e0445a" font-size="11">不限</text>
<rect x="955" y="370" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="965" y="391" fill="#e0445a" font-size="11">不过滤</text>
<!-- row 8 weclaw -->
<text x="20" y="435" fill="#f0eee6" font-size="12.5" font-weight="600">weclaw</text>
<rect x="203" y="414" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="213" y="435" fill="#e0445a" font-size="11">不限</text>
<rect x="391" y="414" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="401" y="435" fill="#e0445a" font-size="11">不限</text>
<rect x="579" y="414" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="589" y="435" fill="#e0445a" font-size="11">不限,审批自动答allow</text>
<rect x="767" y="414" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="777" y="435" fill="#e0445a" font-size="11">不限</text>
<rect x="955" y="414" width="182" height="32" rx="6" fill="#2a1215" stroke="#e0445a"/><text x="965" y="435" fill="#e0445a" font-size="11">全量继承</text>
<!-- row 9 pi-ai -->
<text x="20" y="479" fill="#6f6b5c" font-size="12.5" font-weight="600">pi-ai</text>
<rect x="203" y="458" width="934" height="32" rx="6" fill="#191811" stroke="#3a3729"/>
<text x="213" y="479" fill="#6f6b5c" font-size="11">没有执行面:14个文件,只有provider和协议层,全仓库零个 spawn / child_process。没有可隔离的东西,不参与横向比较。</text>
<!-- row 10 us -->
<line x1="12" y1="500" x2="1148" y2="500" stroke="#2e2c20" stroke-width="1"/>
<text x="20" y="531" fill="#d4843a" font-size="12.5" font-weight="700">OpenProgram</text>
<rect x="203" y="510" width="182" height="32" rx="6" fill="#241f10" stroke="#d4843a" stroke-width="1.6"/><text x="213" y="531" fill="#d4843a" font-size="11">整盘减凭证deny清单</text>
<rect x="391" y="510" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e" stroke-width="1.6"/><text x="401" y="531" fill="#5aad4e" font-size="11">cwd 加临时目录</text>
<rect x="579" y="510" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e" stroke-width="1.6"/><text x="589" y="531" fill="#5aad4e" font-size="11">exec不限,子进程继承</text>
<rect x="767" y="510" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e" stroke-width="1.6"/><text x="777" y="531" fill="#5aad4e" font-size="11">两个平台都断</text>
<rect x="955" y="510" width="182" height="32" rx="6" fill="#101a10" stroke="#5aad4e" stroke-width="1.6"/><text x="965" y="531" fill="#5aad4e" font-size="11">白名单传入</text>
<rect x="12" y="558" width="1136" height="48" rx="9" fill="#16150f" stroke="#3a3729"/>
<text x="32" y="580" fill="#8a8675" font-size="12"><tspan fill="#f0eee6" font-weight="700">当前差异</tspan>:网络断开与环境白名单已实现;读仍采用“整盘可读减凭证deny”,不是容器的最小挂载视图。</text>
<text x="32" y="598" fill="#8a8675" font-size="12">openclaw这一行只描述启用后的容器;它的出厂状态仍是off,关闭时审批默认放行,因此不能用启用后能力代替默认风险。</text>
</svg>
</div>
<div class="tbl">
<table class="wide">
<tr><th>维度</th><th>claude-code</th><th>codex-cli</th><th>openclaw</th><th>pi-mono</th><th>hermes-agent</th><th>opencode</th><th>weclaw</th><th class="us">OpenProgram</th></tr>
<tr><td><b>系统级隔离</b></td><td>外部runtime包</td><td>仓库内自研</td><td>Docker默认;SSH/OpenShell可选</td><td class="m">只有示例扩展</td><td class="m">默认无,后端可选</td><td class="n">明确不做</td><td class="n">主动移除</td><td class="us">Seatbelt / bubblewrap,模型影响面已分类</td></tr>
<tr><td><b>默认开</b></td><td>否</td><td class="y">是,read-only</td><td>否,mode=off</td><td>核心无此概念</td><td>否,默认local</td><td>—</td><td>—</td><td class="us y">是,workspace-write</td></tr>
<tr><td><b>粒度</b></td><td>全局×单命令opt-out</td><td>per-command,工具可覆盖</td><td class="y">全局×agent×tool×session</td><td>整工具级allowlist</td><td>per-pattern加按上下文</td><td>per-tool×per-resource</td><td class="n">无</td><td class="us">全局配置,尚无per-tool覆盖</td></tr>
<tr><td><b>配置入口</b></td><td>分层settings.json</td><td>config.toml加CLI flag</td><td class="y">zod schema生成JSON schema</td><td>settings.json,项目覆盖全局</td><td>cli-config.yaml加环境变量</td><td>opencode.json加frontmatter</td><td class="n">没有安全相关的键</td><td class="us">七个<code>sandbox.*</code>设置键</td></tr>
<tr><td><b>沙箱与审批</b></td><td class="y">沙箱内bash免审批</td><td class="y">里面被拒→问→不带沙箱重跑</td><td class="m">off时审批默认放行;启用后由容器与tool policy处理</td><td>核心没有审批</td><td>守卫就是审批系统</td><td>只有审批</td><td class="n">审批自动答allow</td><td class="us y">scope先判定;仅本地owner可精确单次升级</td></tr>
<tr><td><b>凭证屏蔽出厂状态</b></td><td class="m">引擎有,为空</td><td class="m">引擎有,为空</td><td class="y">启用时挂载源加环境变量过滤</td><td class="y">扩展预置denyRead</td><td class="y">环境变量从注册表推导</td><td>*.env→ask</td><td class="n">无</td><td class="us y">deny-read清单加环境白名单</td></tr>
<tr><td><b>后端不可用</b></td><td>继续执行但明确告警</td><td>静默none,Windows降级</td><td class="y">强制拒绝加doctor预警</td><td class="n">四条路径都静默继续</td><td class="m">非交互fail-open</td><td>—</td><td>—</td><td class="us y">默认refuse,可显式warn</td></tr>
<tr><td><b>审计</b></td><td class="y">内核deny行归因后返回模型</td><td class="y">结构化违规事件加OTel</td><td class="y">专用tool-policy logger</td><td>无</td><td class="y">审批生命周期钩子</td><td>权限事件总线</td><td class="n">只有log.Printf</td><td class="us y">结构化sandbox.violation及审批事件</td></tr>
<tr><td><b>资源限额</b></td><td class="n">无</td><td class="n">仅RLIMIT_CORE=0</td><td class="y">pids/memory/cpus/ulimits,默认不设</td><td class="n">无</td><td class="y">pids 256、tmpfs限额、600秒</td><td class="n">无</td><td class="n">无超时</td><td class="us">CPU/内存/进程数配额不在范围内</td></tr>
<tr><td><b>受管范围</b></td><td>只有Bash和PowerShell</td><td>只有子进程</td><td>启用容器内的一切</td><td>只有bash工具</td><td>terminal和code_execution</td><td>—</td><td>—</td><td class="us y">模型可影响的进程、文件、cron、MCP与嵌套工具面</td></tr>
</table>
</div>
<div class="note"><b>参考实现的边界声明</b>:openclaw面向受信任的单用户operator,审批属于操作防误触机制,不被定义为对抗同一operator的安全边界;
hermes-agent的SECURITY文档同样把模式扫描与审批定义为应用层策略,只有OS级隔离才是执行不可信输入的安全边界。
OpenProgram因此不能把“默认也是off”或“有审批”当成继续延后覆盖面的理由。</div>
<h2 id="best"><span class="num">07</span>独有机制与逐维度比较</h2>
<p class="lead">这一轮新看的五家里,有十一种机制在<code>claude-code</code>和<code>codex-cli</code>里都不存在。
下面八张卡记录逐维度可采用的机制,并标出OpenProgram的采用状态和对应步骤。</p>
<div class="card">
<svg viewBox="0 0 1160 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="八个维度的参考机制与OpenProgram采用状态">
<text x="24" y="30" fill="#d4843a" font-size="16" font-weight="700">八个维度的参考机制与采用结果</text>
<text x="24" y="52" fill="#8a8675" font-size="12.5">每张卡依次记录维度、参考实现、具体机制和OpenProgram采用状态。</text>
<!-- Card 1 -->
<rect x="24" y="72" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="40" y="94" fill="#8a8675" font-size="10.5" letter-spacing="1.6">凭证不进子进程环境</text>
<text x="40" y="118" fill="#5aad4e" font-size="15" font-weight="700">hermes-agent + openclaw</text>
<text x="40" y="140" fill="#a5a190" font-size="11.5">前者从provider注册表推导敏感变量,</text>
<text x="40" y="158" fill="#a5a190" font-size="11.5">后者增加敏感名称正则。OpenProgram</text>
<text x="40" y="176" fill="#8a8675" font-size="11.5">采用更严格的传入白名单并保留名称限制。</text>
<rect x="40" y="192" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="52" y="214" fill="#5aad4e" font-size="12" font-weight="700">已采用 · 第02步</text>
<text x="130" y="214" fill="#8a8675" font-size="11">对应17个凭证目录</text>
<!-- Card 2 -->
<rect x="305" y="72" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="321" y="94" fill="#8a8675" font-size="10.5" letter-spacing="1.6">命令规则匹配之前</text>
<text x="321" y="118" fill="#5aad4e" font-size="15" font-weight="700">hermes-agent 加 openclaw</text>
<text x="321" y="140" fill="#a5a190" font-size="11.5">前者剥ANSI、剥空字节、NFKC归一;</text>
<text x="321" y="158" fill="#a5a190" font-size="11.5">后者按表解包18种启动器,无法证明</text>
<text x="321" y="176" fill="#a5a190" font-size="11.5">无法证明wrapper透明时拒绝,且不保存永久允许。</text>
<rect x="321" y="192" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="333" y="214" fill="#5aad4e" font-size="12" font-weight="700">采用 · 第07步</text>
<text x="431" y="214" fill="#8a8675" font-size="11">规则规范化</text>
<!-- Card 3 -->
<rect x="586" y="72" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="602" y="94" fill="#8a8675" font-size="10.5" letter-spacing="1.6">bypass不能跳过的强制规则</text>
<text x="602" y="118" fill="#5aad4e" font-size="15" font-weight="700">hermes-agent</text>
<text x="602" y="140" fill="#a5a190" font-size="11.5">删根、mkfs、写裸设备、关机这一类,</text>
<text x="602" y="158" fill="#a5a190" font-size="11.5">在读取yolo和mode=off之前即拒绝,</text>
<text x="602" y="176" fill="#a5a190" font-size="11.5">拒绝信息明确说明任何设置均不能覆盖。</text>
<rect x="602" y="192" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="614" y="214" fill="#5aad4e" font-size="12" font-weight="700">采用 · 第04步</text>
<text x="692" y="214" fill="#8a8675" font-size="11">对应bypass短路</text>
<!-- Card 4 -->
<rect x="867" y="72" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="883" y="94" fill="#8a8675" font-size="10.5" letter-spacing="1.6">按上下文的审批默认值</text>
<text x="883" y="118" fill="#5aad4e" font-size="15" font-weight="700">hermes-agent</text>
<text x="883" y="140" fill="#a5a190" font-size="11.5">五种上下文五种策略:cron默认deny,</text>
<text x="883" y="158" fill="#a5a190" font-size="11.5">subagent自动deny并留审计行,理由是</text>
<text x="883" y="176" fill="#a5a190" font-size="11.5">工作线程无法提供交互,等待会阻塞父进程。</text>
<rect x="883" y="192" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="895" y="214" fill="#5aad4e" font-size="12" font-weight="700">采用 · 第04/05步</text>
<text x="1020" y="214" fill="#8a8675" font-size="11">ask→deny</text>
<!-- Card 5 -->
<rect x="24" y="256" width="267" height="172" rx="10" fill="#12110b" stroke="#9d6fe0" stroke-width="1.4"/>
<text x="40" y="278" fill="#8a8675" font-size="10.5" letter-spacing="1.6">嵌套agent自带的执行面</text>
<text x="40" y="302" fill="#9d6fe0" font-size="15" font-weight="700">openclaw</text>
<text x="40" y="324" fill="#a5a190" font-size="11.5">不限制整个嵌套进程,在协议层对每个</text>
<text x="40" y="342" fill="#a5a190" font-size="11.5">方法分类,拒绝command/和fs/,</text>
<text x="40" y="360" fill="#a5a190" font-size="11.5">注入由宿主沙箱执行的替代工具。</text>
<rect x="40" y="376" width="235" height="34" rx="7" fill="#1a1424" stroke="#9d6fe0"/>
<text x="52" y="398" fill="#9d6fe0" font-size="12" font-weight="700">采用 · 第06步</text>
<text x="152" y="398" fill="#8a8675" font-size="11">协议级受管工具</text>
<!-- Card 6 -->
<rect x="305" y="256" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="321" y="278" fill="#8a8675" font-size="10.5" letter-spacing="1.6">机制不可用时怎么办</text>
<text x="321" y="302" fill="#5aad4e" font-size="15" font-weight="700">openclaw</text>
<text x="321" y="324" fill="#a5a190" font-size="11.5">Docker缺失时拒绝并说明缺失依赖;</text>
<text x="321" y="342" fill="#a5a190" font-size="11.5">镜像缺失时不使用通用镜像替代;</text>
<text x="321" y="360" fill="#a5a190" font-size="11.5">另有doctor在执行前检查。</text>
<rect x="321" y="376" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="333" y="398" fill="#5aad4e" font-size="12" font-weight="700">已采用 · 第03步</text>
<text x="438" y="398" fill="#8a8675" font-size="11">默认refuse</text>
<!-- Card 7 -->
<rect x="586" y="256" width="267" height="172" rx="10" fill="#12110b" stroke="#5aad4e" stroke-width="1.4"/>
<text x="602" y="278" fill="#8a8675" font-size="10.5" letter-spacing="1.6">隔离后端不可用时的行为</text>
<text x="602" y="302" fill="#5aad4e" font-size="15" font-weight="700">pi-mono</text>
<text x="602" y="324" fill="#a5a190" font-size="11.5">拦截器抛非Error异常时转成"扩展失败,</text>
<text x="602" y="342" fill="#a5a190" font-size="11.5">阻止执行";随包的示例在没有UI时直接</text>
<text x="602" y="360" fill="#a5a190" font-size="11.5">拒绝;非交互环境默认deny。</text>
<rect x="602" y="376" width="235" height="34" rx="7" fill="#101a10" stroke="#5aad4e"/>
<text x="614" y="398" fill="#5aad4e" font-size="12" font-weight="700">已采用 · 第03步</text>
<text x="719" y="398" fill="#8a8675" font-size="11">后端不可用</text>
<!-- Card 8 -->
<rect x="867" y="256" width="267" height="172" rx="10" fill="#12110b" stroke="#2db3d5" stroke-width="1.4"/>
<text x="883" y="278" fill="#8a8675" font-size="10.5" letter-spacing="1.6">对自己的安全配置做检查</text>
<text x="883" y="302" fill="#2db3d5" font-size="15" font-weight="700">openclaw</text>
<text x="883" y="324" fill="#a5a190" font-size="11.5">一批具名检查项跑在用户配置上,其中一条</text>
<text x="883" y="342" fill="#a5a190" font-size="11.5">专门覆盖"设置里写着sandbox而有效模式</text>
<text x="883" y="360" fill="#a5a190" font-size="11.5">是off",正是我们已经有的那类缺陷。</text>
<rect x="883" y="376" width="235" height="34" rx="7" fill="#111d21" stroke="#2db3d5"/>
<text x="895" y="398" fill="#2db3d5" font-size="12" font-weight="700">采用 · 第08步</text>
<text x="994" y="398" fill="#8a8675" font-size="11">默认前预检</text>
<!-- 反面案例 -->
<rect x="24" y="444" width="1110" height="100" rx="10" fill="#1c1116" stroke="#e0445a" stroke-width="1.3"/>
<text x="44" y="468" fill="#e0445a" font-size="13.5" font-weight="700">三条不采用的机制及其对应风险</text>
<text x="44" y="494" fill="#f0eee6" font-size="12" font-weight="600">pi-mono:项目配置覆盖全局</text>
<text x="44" y="514" fill="#a5a190" font-size="11.5">clone下来的仓库带一个配置文件就能</text>
<text x="44" y="532" fill="#a5a190" font-size="11.5">关闭全局沙箱,项目级扩展也没有信任验证。</text>
<text x="404" y="494" fill="#f0eee6" font-size="12" font-weight="600">pi-mono:截断后把全文路径给模型</text>
<text x="404" y="514" fill="#a5a190" font-size="11.5">输出按2000行截断,全文写进临时文件</text>
<text x="404" y="532" fill="#a5a190" font-size="11.5">并把路径返回,模型可再次读取未截断内容。</text>
<text x="764" y="494" fill="#f0eee6" font-size="12" font-weight="600">weclaw:聊天适配器自动批准审批</text>
<text x="764" y="514" fill="#a5a190" font-size="11.5">同一个ACP协议,hermes转发给客户端,</text>
<text x="764" y="532" fill="#a5a190" font-size="11.5">weclaw自己替客户端答了,还没有发送者名单。</text>
</svg>
</div>
<h2 id="cred"><span class="num">08</span>凭证读取不能由网络隔离替代</h2>
<p class="lead">子进程无网不表示读取到的凭证不会离开子进程:工具输出会返回agent runtime,并由宿主网络进入下一次模型请求。
因此凭证读取限制、环境过滤和执行面覆盖都必须独立存在。</p>
<div class="card">
<svg viewBox="0 0 1160 402" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="凭证读取经过工具输出与宿主模型请求离开子进程">
<defs>
<marker id="c-gy" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#8a8675"/></marker>
<marker id="c-rd" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#e0445a"/></marker>
<marker id="c-gn" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#5aad4e"/></marker>
</defs>
<text x="24" y="30" fill="#d4843a" font-size="15.5" font-weight="700">子进程网络被拒绝后,凭证仍可经工具结果进入模型请求</text>
<!-- Row A -->
<text x="24" y="66" fill="#8a8675" font-size="11.5" letter-spacing="1.5">空deny列表不能依赖网络沙箱证明安全</text>
<rect x="24" y="76" width="196" height="66" rx="9" fill="#12110b" stroke="#3a3729"/>
<text x="122" y="104" fill="#f0eee6" font-size="12.5" font-weight="700" text-anchor="middle">整盘可读</text>
<text x="122" y="126" fill="#8a8675" font-size="11.5" text-anchor="middle">凭证都读得到</text>
<line x1="220" y1="109" x2="246" y2="109" stroke="#8a8675" stroke-width="1.5" marker-end="url(#c-gy)"/>
<rect x="254" y="76" width="196" height="66" rx="9" fill="#12110b" stroke="#3a3729"/>
<text x="352" y="104" fill="#f0eee6" font-size="12.5" font-weight="700" text-anchor="middle">工具输出返回runtime</text>
<text x="352" y="126" fill="#8a8675" font-size="11.5" text-anchor="middle">不需要子进程发起网络请求</text>
<line x1="450" y1="109" x2="476" y2="109" stroke="#8a8675" stroke-width="1.5" marker-end="url(#c-gy)"/>
<rect x="484" y="76" width="196" height="66" rx="9" fill="#12110b" stroke="#3a3729"/>
<text x="582" y="104" fill="#f0eee6" font-size="12.5" font-weight="700" text-anchor="middle">宿主构造模型请求</text>
<text x="582" y="126" fill="#8a8675" font-size="11.5" text-anchor="middle">宿主网络不在子进程命名空间内</text>