Skip to content

Commit e91b462

Browse files
committed
feat: add access control
1 parent b246ac1 commit e91b462

File tree

4 files changed

+189
-66
lines changed

4 files changed

+189
-66
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Live: [https://tape.systems](https://tape.systems)
44

55
A minimal site about constructing context from tape, practiced in [bub.build](https://bub.build).
66

7-
The site covers both the core context model and three natural extensions: observability, evaluation, and model training.
7+
The site covers both the core context model and four natural extensions: access control, observability, evaluation, and model training.
88

99
References:
1010

@@ -18,6 +18,7 @@ References:
1818
- Context Strategies: compact, summary, fork-merge
1919
- Advanced Collaboration: memory, teams
2020
- Appendix:
21+
- access control via database-native tenant boundaries and audit reads
2122
- observability via replayable tape timelines
2223
- evaluation via anchor-bounded views, judges, and derived annotations
2324
- training via anchor-segmented trajectory export

components/appendix.tsx

Lines changed: 156 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,24 @@ export function Appendix() {
1919
<div className="flex flex-col gap-8">
2020
<AppendixCard
2121
label="A"
22+
title={t("appendix.access.title")}
23+
description={t("appendix.access.desc")}
24+
note={t("appendix.access.note")}
25+
diagram={<AccessControlDiagram />}
26+
/>
27+
28+
<AppendixCard
29+
label="B"
2230
title={t("appendix.observability.title")}
2331
description={t("appendix.observability.desc")}
2432
note={t("appendix.observability.note")}
2533
referenceLabel={t("appendix.observability.ref")}
26-
referenceHref="https://github.com/bubbuild/bub"
34+
referenceHref="https://bub.build/architecture/"
2735
diagram={<ObservabilityDiagram />}
2836
/>
2937

3038
<AppendixCard
31-
label="B"
39+
label="C"
3240
title={t("appendix.eval.title")}
3341
description={t("appendix.eval.desc")}
3442
note={t("appendix.eval.note")}
@@ -38,7 +46,7 @@ export function Appendix() {
3846
/>
3947

4048
<AppendixCard
41-
label="C"
49+
label="D"
4250
title={t("appendix.training.title")}
4351
description={
4452
<>
@@ -79,8 +87,8 @@ function AppendixCard({
7987
title: string
8088
description: ReactNode
8189
note: string
82-
referenceLabel: string
83-
referenceHref: string
90+
referenceLabel?: string
91+
referenceHref?: string
8492
diagram: ReactNode
8593
}) {
8694
const { locale } = useI18n()
@@ -99,30 +107,129 @@ function AppendixCard({
99107

100108
<div className="bg-foreground px-5 py-3 flex flex-col md:flex-row md:items-center md:justify-between gap-2">
101109
<p className="text-xs font-mono text-background/70 leading-relaxed">{note}</p>
102-
<a
103-
href={referenceHref}
104-
target="_blank"
105-
rel="noopener noreferrer"
106-
className="inline-flex items-center gap-2 text-[11px] font-mono text-background/50 hover:text-background/75 transition-colors"
107-
>
108-
<span>{locale === "zh" ? "参考" : "ref"}</span>
109-
<span>{referenceLabel}</span>
110-
<svg width="10" height="10" viewBox="0 0 10 10" className="text-current">
111-
<path
112-
d="M 2 8 L 8 2 M 4 2 L 8 2 L 8 6"
113-
stroke="currentColor"
114-
strokeWidth="1.2"
115-
fill="none"
116-
strokeLinecap="round"
117-
strokeLinejoin="round"
118-
/>
119-
</svg>
120-
</a>
110+
{referenceLabel && referenceHref ? (
111+
<a
112+
href={referenceHref}
113+
target="_blank"
114+
rel="noopener noreferrer"
115+
className="inline-flex items-center gap-2 text-[11px] font-mono text-background/50 hover:text-background/75 transition-colors"
116+
>
117+
<span>{locale === "zh" ? "参考" : "ref"}</span>
118+
<span>{referenceLabel}</span>
119+
<svg width="10" height="10" viewBox="0 0 10 10" className="text-current">
120+
<path
121+
d="M 2 8 L 8 2 M 4 2 L 8 2 L 8 6"
122+
stroke="currentColor"
123+
strokeWidth="1.2"
124+
fill="none"
125+
strokeLinecap="round"
126+
strokeLinejoin="round"
127+
/>
128+
</svg>
129+
</a>
130+
) : null}
121131
</div>
122132
</div>
123133
)
124134
}
125135

136+
function AccessControlDiagram() {
137+
const { locale, t } = useI18n()
138+
139+
return (
140+
<svg viewBox="0 0 700 240" className="w-full" fill="none">
141+
<defs>
142+
<marker id="acl-arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
143+
<path d="M0 1L9 5L0 9" className="fill-none stroke-foreground" strokeWidth="1.5" />
144+
</marker>
145+
<marker id="acl-arr-a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
146+
<path d="M0 1L9 5L0 9" className="fill-none stroke-accent" strokeWidth="1.5" />
147+
</marker>
148+
</defs>
149+
150+
<rect x="28" y="90" width="120" height="64" rx="12" className="fill-foreground" />
151+
<text x="88" y="114" textAnchor="middle" className="fill-primary-foreground text-[11px] font-mono font-semibold">
152+
{locale === "zh" ? "Admin" : "Admin"}
153+
</text>
154+
<text x="88" y="132" textAnchor="middle" className="fill-primary-foreground/70 text-[8px] font-mono">
155+
{locale === "zh" ? "audit scope" : "audit scope"}
156+
</text>
157+
<text x="88" y="144" textAnchor="middle" className="fill-primary-foreground/70 text-[8px] font-mono">
158+
{locale === "zh" ? "read role" : "read role"}
159+
</text>
160+
161+
<rect x="196" y="70" width="152" height="104" rx="12" className="fill-secondary/35 stroke-border" strokeWidth="1" />
162+
<text x="272" y="98" textAnchor="middle" className="fill-muted-foreground text-[10px] font-mono font-semibold">
163+
{locale === "zh" ? "Boundary" : "Boundary"}
164+
</text>
165+
{[
166+
{ y: 110, label: locale === "zh" ? "owner" : "owner" },
167+
{ y: 134, label: locale === "zh" ? "grant" : "grant" },
168+
{ y: 158, label: locale === "zh" ? "read-only" : "read-only" },
169+
].map((item) => (
170+
<g key={item.label}>
171+
<rect x="224" y={item.y} width="96" height="14" rx="5" className="fill-card stroke-border" strokeWidth="0.8" />
172+
<text x="272" y={item.y + 9} textAnchor="middle" className="fill-muted-foreground text-[7px] font-mono">
173+
{item.label}
174+
</text>
175+
</g>
176+
))}
177+
178+
<rect x="390" y="36" width="132" height="78" rx="12" className="fill-card stroke-border" strokeWidth="1" />
179+
<text x="456" y="58" textAnchor="middle" className="fill-foreground text-[10px] font-mono font-semibold">
180+
{locale === "zh" ? "User A" : "User A"}
181+
</text>
182+
<text x="456" y="72" textAnchor="middle" className="fill-muted-foreground text-[8px] font-mono">
183+
{locale === "zh" ? "Database" : "Database"}
184+
</text>
185+
<rect x="416" y="80" width="80" height="20" rx="6" className="fill-secondary/30 stroke-border" strokeWidth="1" />
186+
<text x="456" y="93" textAnchor="middle" className="fill-foreground text-[8px] font-mono font-semibold">
187+
{locale === "zh" ? "Tape Table" : "Tape Table"}
188+
</text>
189+
190+
<rect x="390" y="126" width="132" height="78" rx="12" className="fill-card stroke-border" strokeWidth="1" />
191+
<text x="456" y="148" textAnchor="middle" className="fill-foreground text-[10px] font-mono font-semibold">
192+
{locale === "zh" ? "User B" : "User B"}
193+
</text>
194+
<text x="456" y="162" textAnchor="middle" className="fill-muted-foreground text-[8px] font-mono">
195+
{locale === "zh" ? "Database" : "Database"}
196+
</text>
197+
<rect x="416" y="170" width="80" height="20" rx="6" className="fill-secondary/30 stroke-border" strokeWidth="1" />
198+
<text x="456" y="183" textAnchor="middle" className="fill-foreground text-[8px] font-mono font-semibold">
199+
{locale === "zh" ? "Tape Table" : "Tape Table"}
200+
</text>
201+
202+
<rect x="560" y="84" width="112" height="76" rx="12" className="fill-accent/10 stroke-accent" strokeWidth="1" />
203+
<text x="616" y="108" textAnchor="middle" className="fill-accent text-[10px] font-mono font-semibold">
204+
{locale === "zh" ? "Audit" : "Audit"}
205+
</text>
206+
<text x="616" y="126" textAnchor="middle" className="fill-accent/70 text-[8px] font-mono">
207+
{locale === "zh" ? "read-only" : "read-only"}
208+
</text>
209+
<text x="616" y="142" textAnchor="middle" className="fill-accent/70 text-[8px] font-mono">
210+
{locale === "zh" ? "multi-user" : "multi-user"}
211+
</text>
212+
213+
<line x1="148" y1="122" x2="196" y2="122" className="stroke-muted-foreground/40" strokeWidth="1" markerEnd="url(#acl-arr)" />
214+
215+
<line x1="348" y1="122" x2="366" y2="122" className="stroke-muted-foreground/35" strokeWidth="1" />
216+
<line x1="366" y1="90" x2="366" y2="166" className="stroke-muted-foreground/25" strokeWidth="1" />
217+
<line x1="366" y1="90" x2="390" y2="90" className="stroke-muted-foreground/35" strokeWidth="1" markerEnd="url(#acl-arr)" />
218+
<line x1="366" y1="166" x2="390" y2="166" className="stroke-muted-foreground/35" strokeWidth="1" markerEnd="url(#acl-arr)" />
219+
220+
<line x1="522" y1="90" x2="540" y2="90" className="stroke-accent/60" strokeWidth="1" />
221+
<line x1="522" y1="180" x2="540" y2="180" className="stroke-accent/60" strokeWidth="1" />
222+
<line x1="540" y1="90" x2="540" y2="180" className="stroke-accent/35" strokeWidth="1" />
223+
<line x1="540" y1="122" x2="560" y2="122" className="stroke-accent/60" strokeWidth="1" markerEnd="url(#acl-arr-a)" />
224+
225+
<line x1="560" y1="176" x2="672" y2="176" className="stroke-muted-foreground/20" strokeWidth="1" strokeDasharray="4 3" />
226+
<text x="616" y="168" textAnchor="middle" className="fill-muted-foreground/45 text-[7px] font-mono">
227+
{t("appendix.access.writeBlock")}
228+
</text>
229+
</svg>
230+
)
231+
}
232+
126233
function ObservabilityDiagram() {
127234
const { locale } = useI18n()
128235

@@ -147,13 +254,7 @@ function ObservabilityDiagram() {
147254
Tape
148255
</text>
149256

150-
<rect x="548" y="18" width="122" height="34" rx="7" className="fill-accent" />
151-
<text x="609" y="40" textAnchor="middle" className="fill-accent-foreground text-[12px] font-mono font-semibold">
152-
Web UI
153-
</text>
154-
155257
<line x1="148" y1="35" x2="198" y2="35" className="stroke-foreground" strokeWidth="1.2" markerEnd="url(#obs-arr)" />
156-
<line x1="344" y1="35" x2="538" y2="35" className="stroke-accent" strokeWidth="1.2" markerEnd="url(#obs-arr-a)" />
157258

158259
{[
159260
{ x: 224, y: 82, label: locale === "zh" ? "anchor" : "anchor", className: "fill-accent/12 stroke-accent" },
@@ -170,42 +271,41 @@ function ObservabilityDiagram() {
170271
))}
171272

172273
<text x="276" y="70" textAnchor="middle" className="fill-muted-foreground/45 text-[9px] font-mono">
173-
{locale === "zh" ? "append-only trace" : "append-only trace"}
274+
{locale === "zh" ? "same append-only facts" : "same append-only facts"}
174275
</text>
175276

176-
<rect x="420" y="88" width="92" height="96" rx="10" className="fill-secondary/35 stroke-border" strokeWidth="1" />
177-
<text x="466" y="108" textAnchor="middle" className="fill-muted-foreground text-[10px] font-mono font-semibold">
178-
{locale === "zh" ? "filters" : "filters"}
277+
<rect x="378" y="78" width="128" height="106" rx="12" className="fill-secondary/35 stroke-border" strokeWidth="1" />
278+
<text x="442" y="100" textAnchor="middle" className="fill-muted-foreground text-[10px] font-mono font-semibold">
279+
{locale === "zh" ? "derived views" : "derived views"}
179280
</text>
180-
{["session", "tool", "event"].map((item, index) => (
281+
{["timeline", "replay", "qa context"].map((item, index) => (
181282
<g key={item}>
182-
<rect x="435" y={120 + index * 18} width="62" height="12" rx="4" className="fill-card stroke-border" strokeWidth="0.8" />
183-
<text x="466" y={129 + index * 18} textAnchor="middle" className="fill-muted-foreground text-[7px] font-mono">
283+
<rect x="400" y={114 + index * 20} width="84" height="14" rx="5" className="fill-card stroke-border" strokeWidth="0.8" />
284+
<text x="442" y={123 + index * 20} textAnchor="middle" className="fill-muted-foreground text-[7px] font-mono">
184285
{item}
185286
</text>
186287
</g>
187288
))}
188289

189-
{[
190-
{ y: 86, title: locale === "zh" ? "Timeline" : "Timeline", detail: locale === "zh" ? "turn / tool / event" : "turn / tool / event" },
191-
{ y: 126, title: locale === "zh" ? "Replay" : "Replay", detail: locale === "zh" ? "inspect exact path" : "inspect exact path" },
192-
{ y: 166, title: locale === "zh" ? "Usage" : "Usage", detail: locale === "zh" ? "token + anchor stats" : "token + anchor stats" },
193-
].map((panel) => (
194-
<g key={panel.title}>
195-
<rect x="556" y={panel.y} width="106" height="28" rx="6" className="fill-accent/10 stroke-accent" strokeWidth="1" />
196-
<text x="568" y={panel.y + 12} className="fill-accent text-[9px] font-mono font-semibold">
197-
{panel.title}
198-
</text>
199-
<text x="568" y={panel.y + 21} className="fill-accent/60 text-[7px] font-mono">
200-
{panel.detail}
201-
</text>
202-
</g>
203-
))}
290+
<rect x="548" y="104" width="108" height="28" rx="8" className="fill-accent/12 stroke-accent" strokeWidth="1" />
291+
<text x="562" y="116" className="fill-accent text-[9px] font-mono font-semibold">
292+
{locale === "zh" ? "Trace UI" : "Trace UI"}
293+
</text>
294+
<text x="562" y="125" className="fill-accent/65 text-[7px] font-mono">
295+
{locale === "zh" ? "timeline / replay" : "timeline / replay"}
296+
</text>
297+
298+
<rect x="548" y="144" width="108" height="28" rx="8" className="fill-secondary/40 stroke-border" strokeWidth="1" />
299+
<text x="562" y="156" className="fill-foreground text-[9px] font-mono font-semibold">
300+
{locale === "zh" ? "Ask Bub" : "Ask Bub"}
301+
</text>
302+
<text x="562" y="165" className="fill-muted-foreground text-[7px] font-mono">
303+
{locale === "zh" ? "inspect / explain" : "inspect / explain"}
304+
</text>
204305

205-
<line x1="328" y1="124" x2="420" y2="124" className="stroke-muted-foreground/35" strokeWidth="1" strokeDasharray="4 3" markerEnd="url(#obs-arr)" />
206-
<line x1="512" y1="136" x2="556" y2="100" className="stroke-accent/55" strokeWidth="1" markerEnd="url(#obs-arr-a)" />
207-
<line x1="512" y1="148" x2="556" y2="140" className="stroke-accent/55" strokeWidth="1" markerEnd="url(#obs-arr-a)" />
208-
<line x1="512" y1="160" x2="556" y2="180" className="stroke-accent/55" strokeWidth="1" markerEnd="url(#obs-arr-a)" />
306+
<line x1="328" y1="124" x2="378" y2="124" className="stroke-muted-foreground/35" strokeWidth="1" strokeDasharray="4 3" markerEnd="url(#obs-arr)" />
307+
<line x1="506" y1="126" x2="548" y2="118" className="stroke-accent/55" strokeWidth="1" markerEnd="url(#obs-arr-a)" />
308+
<line x1="506" y1="146" x2="548" y2="158" className="stroke-foreground/45" strokeWidth="1" markerEnd="url(#obs-arr)" />
209309
</svg>
210310
)
211311
}

components/reading-path.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ function ReadingDirectory() {
100100
num: "07",
101101
href: "#appendix",
102102
title: locale === "zh" ? "附录" : "Appendix",
103-
detail: "observability / eval / training",
103+
detail: "access / observability / eval / training",
104104
group: locale === "zh" ? "附加" : "Applied",
105105
},
106106
]

lib/i18n.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -278,19 +278,41 @@ const translations: Translations = {
278278
// Appendix
279279
"appendix.title": { zh: "附录", en: "Appendix" },
280280
"appendix.subtitle": {
281-
zh: "三个外延:把 tape 作为可观测层、评估层,以及训练轨迹底座。",
282-
en: "Three extensions: observability, eval, and training.",
283-
},
281+
zh: "四个外延:把 tape 作为权限边界、可观测层、评估层,以及训练轨迹底座。",
282+
en: "Four extensions: access control, observability, eval, and training.",
283+
},
284+
"appendix.access.title": { zh: "权限管理", en: "Access Control" },
285+
"appendix.access.desc": {
286+
zh: "如果一个租户拥有一个库,而 tape 只是其中一张表,那么隔离边界天然就是数据库与表本身。审计子帐号时,只需授予目标 tape table 或只读 view 的显式读取权限。",
287+
en: "If each tenant owns a database and tape is just one table inside it, the database and table already define the isolation boundary. Auditing a child account only needs explicit read access to the target tape table or a read-only view.",
288+
},
289+
"appendix.access.note": {
290+
zh: "权限源头仍应是数据库对象本身。审计访问应使用受约束的只读角色,而不是复用 owner 身份。",
291+
en: "Keep the database objects themselves as the source of truth. Audit access should use constrained read-only roles instead of reusing the owner identity.",
292+
},
293+
"appendix.access.ref": { zh: "database privileges", en: "database privileges" },
294+
"appendix.access.audit": { zh: "租户审计视图", en: "tenant audit view" },
295+
"appendix.access.parent": { zh: "授权租户", en: "authorized tenant" },
296+
"appendix.access.child": { zh: "子帐号", en: "child account" },
297+
"appendix.access.db": { zh: "database ownership", en: "database ownership" },
298+
"appendix.access.isolation": { zh: "database boundary", en: "database boundary" },
299+
"appendix.access.inherit": { zh: "table-level read", en: "table-level read" },
300+
"appendix.access.auditDetail": { zh: "read-only tape audit", en: "read-only tape audit" },
301+
"appendix.access.writeBlock": { zh: "no owner / no write", en: "no owner / no write" },
302+
"appendix.access.policy": { zh: "reuse DB owner + GRANT", en: "reuse DB owner + GRANT" },
303+
"appendix.access.detail1": { zh: "connect to child DB", en: "connect to child DB" },
304+
"appendix.access.detail2": { zh: "select tape table / view", en: "select tape table / view" },
305+
"appendix.access.detail3": { zh: "preserve DB isolation", en: "preserve DB isolation" },
284306
"appendix.observability.title": { zh: "可观测性", en: "Observability" },
285307
"appendix.observability.desc": {
286-
zh: "tape 不只服务上下文装配,也可以保留 session、tool call 和运行事件,再由 web UI 组装成可检索、可回放的时间线。",
287-
en: "Tape can retain sessions, tool calls, and events for a replayable web timeline.",
308+
zh: "tape 不只服务上下文装配,也可以保留 session、tool call 和运行事件。同一批 append-only facts 既能被 UI 回放,也能被 bub 读取并解释发生了什么。",
309+
en: "Tape can retain sessions, tool calls, and runtime events. The same append-only facts can power replay in the UI or let bub explain what happened.",
288310
},
289311
"appendix.observability.note": {
290-
zh: "UI 是派生视图;原始事实仍留在 append-only tape。",
291-
en: "The UI is a derived view; raw facts remain in the append-only tape.",
312+
zh: "上下文装配与可观测性消费的是同一条 append-only tape,所有出口都建立在 derived views 上。所以除了看 UI,也可以直接询问 bub。",
313+
en: "Context assembly and observability consume the same append-only tape, and every outlet is built from derived views. So besides the UI, you can ask bub directly.",
292314
},
293-
"appendix.observability.ref": { zh: "bub", en: "bub" },
315+
"appendix.observability.ref": { zh: "bub architecture", en: "bub architecture" },
294316
"appendix.eval.title": { zh: "Eval", en: "Eval" },
295317
"appendix.eval.desc": {
296318
zh: "按 anchor 取片段,回放历史,检查决策;评分与标签作为派生事实写回。",

0 commit comments

Comments
 (0)