Merge pull request #5520 from Lstarsky0/feat/docs-sandbox-web
feat(web): move docs/sandbox and docs/web onto the dictionary spine (#5337) Preserves Lstarsky0's original commits. Same merge shape as #5517.
This commit is contained in:
@@ -247,6 +247,10 @@ abort under load.
|
||||
- `feat(web): move docs/hooks and docs/troubleshooting onto the dictionary
|
||||
spine` (Lstarsky0) — both pages now read copy from the locale dictionaries
|
||||
instead of inline bilingual literals (#5337).
|
||||
- `feat(web): move docs/constitution and docs/runtime-api onto the dictionary
|
||||
spine` (Lstarsky0) — both pages now read copy from the locale dictionaries
|
||||
instead of inline bilingual literals; another incremental phase of #5337,
|
||||
not completion of the full epic (#5517).
|
||||
- `docs(i18n): complete Tier 1 of Chinese docs localization` (SparkofSpike) —
|
||||
Chinese and Indonesian docs move to `docs/zh_hans/` and `docs/id/`, with
|
||||
redirect stubs at the old paths for one release cycle (#5482).
|
||||
|
||||
@@ -247,6 +247,10 @@ abort under load.
|
||||
- `feat(web): move docs/hooks and docs/troubleshooting onto the dictionary
|
||||
spine` (Lstarsky0) — both pages now read copy from the locale dictionaries
|
||||
instead of inline bilingual literals (#5337).
|
||||
- `feat(web): move docs/constitution and docs/runtime-api onto the dictionary
|
||||
spine` (Lstarsky0) — both pages now read copy from the locale dictionaries
|
||||
instead of inline bilingual literals; another incremental phase of #5337,
|
||||
not completion of the full epic (#5517).
|
||||
- `docs(i18n): complete Tier 1 of Chinese docs localization` (SparkofSpike) —
|
||||
Chinese and Indonesian docs move to `docs/zh_hans/` and `docs/id/`, with
|
||||
redirect stubs at the old paths for one release cycle (#5482).
|
||||
|
||||
@@ -1,114 +1,61 @@
|
||||
import { Fragment } from "react";
|
||||
import { getDocsSandbox, splitTokens } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
/**
|
||||
* The config values the policies paragraph typesets as inline `<code>`.
|
||||
* `docs/VOICE.md` keeps key names code-owned, so the dictionaries carry
|
||||
* `{token}`s rather than the literals.
|
||||
*/
|
||||
const CODE_SPANS: Record<string, string> = {
|
||||
sandboxMode: "sandbox_mode",
|
||||
readOnly: "read-only",
|
||||
workspaceWrite: "workspace-write",
|
||||
dangerFullAccess: "danger-full-access",
|
||||
externalSandbox: "external-sandbox",
|
||||
};
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const t = getDocsSandbox(locale);
|
||||
return buildPageMetadata({
|
||||
path: "/docs/sandbox",
|
||||
locale,
|
||||
title: isZh ? "沙箱与审批 · Codewhale 文档" : "Sandbox & Approval · Codewhale Docs",
|
||||
description: isZh
|
||||
? "macOS Seatbelt、Linux 可选 bubblewrap、平台缺口和审批策略的真实边界。"
|
||||
: "The honest boundary: macOS Seatbelt, opt-in Linux bubblewrap, platform gaps, and approval policy.",
|
||||
title: t.metaTitle,
|
||||
description: t.metaDescription,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function SandboxPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const bodyClass = isZh
|
||||
? "text-ink-soft leading-[1.9] tracking-wide"
|
||||
: "text-ink-soft leading-relaxed";
|
||||
const platforms = isZh
|
||||
? [
|
||||
{
|
||||
name: "macOS · Seatbelt",
|
||||
detail:
|
||||
"Codewhale 探测 /usr/bin/sandbox-exec;探测成功且策略要求沙箱时,子命令会被包上运行时生成的 Seatbelt profile:广泛的文件系统读取、按策略限制的写入、仅在策略允许时放行网络。探测失败则如实报告无 OS 沙箱。",
|
||||
},
|
||||
{
|
||||
name: "Linux · 可选 bubblewrap",
|
||||
detail:
|
||||
"Linux 命令沙箱是显式启用的:设置 prefer_bwrap = true,且 /usr/bin/bwrap 是可执行文件时才选用。子命令得到只读根视图,writable 挂载来自解析后的策略;默认隔离网络命名空间,仅在策略开启 network_access 时加 --share-net。未启用或未安装 bwrap 时报告 none。",
|
||||
},
|
||||
{
|
||||
name: "Windows · 无 OS 沙箱",
|
||||
detail:
|
||||
"Windows 命令路径目前报告无 OS 沙箱。主机权限和审批策略仍然有效,但它们不是 Codewhale 的 OS 命令沙箱。",
|
||||
},
|
||||
{
|
||||
name: "外部 OpenSandbox 执行",
|
||||
detail:
|
||||
"配置 sandbox_backend = \"opensandbox\" 后,shell 执行会发往配置的 OpenSandbox 兼容 HTTP 端点,而不是启动本地子进程。隔离保证属于所配置的服务及其运营者。",
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: "macOS · Seatbelt",
|
||||
detail:
|
||||
"Codewhale probes /usr/bin/sandbox-exec; when the probe succeeds and the policy requests a sandbox, the child command is wrapped in a generated Seatbelt profile: broad filesystem reads, policy-limited writes, and network only when the policy enables it. A failed probe is reported honestly as no OS sandbox.",
|
||||
},
|
||||
{
|
||||
name: "Linux · opt-in bubblewrap",
|
||||
detail:
|
||||
"Linux command sandboxing is opt-in: set prefer_bwrap = true and keep /usr/bin/bwrap executable. The child gets a read-only root view with writable mounts derived from the resolved policy; the network namespace is isolated by default and --share-net is added only when the policy enables network access. Without the opt-in, Codewhale reports none.",
|
||||
},
|
||||
{
|
||||
name: "Windows · no OS sandbox",
|
||||
detail:
|
||||
"The Windows command path currently reports no OS sandbox. Host permissions and approval policy still apply, but they are not a Codewhale OS command sandbox.",
|
||||
},
|
||||
{
|
||||
name: "External OpenSandbox execution",
|
||||
detail:
|
||||
"With sandbox_backend = \"opensandbox\", shell execution is sent to the configured OpenSandbox-compatible HTTP endpoint instead of starting a local child. Isolation guarantees belong to the configured service and its operator.",
|
||||
},
|
||||
];
|
||||
const t = getDocsSandbox(locale);
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
<section id="overview" className="scroll-mt-32">
|
||||
<h1 className="font-display text-3xl mb-1">{isZh ? "沙箱与审批" : "Sandbox & Approval"}</h1>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "Codewhale 可以启动由模型提出的 shell 命令。审批策略、感知工作区的文件工具和操作系统命令包装器是三个独立的控制:一次审批不是沙箱,选择 workspace-write 也不代表当前平台有可用的 OS 包装器。本页只描述已经接入命令执行路径的行为。"
|
||||
: "Codewhale can launch shell commands proposed by a model. Approval policy, workspace-aware tools, and an operating-system command wrapper are separate controls: an approval is not a sandbox, and selecting workspace-write does not prove the current platform has an OS wrapper available. This page describes only behavior wired into the command execution path."}
|
||||
</p>
|
||||
<h1 className="font-display text-3xl mb-1">{t.overviewTitle}</h1>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.overviewLead}</p>
|
||||
<div className="hairline-t mt-6">
|
||||
{platforms.map((row) => (
|
||||
<section key={row.name} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-lg">{row.name}</h3>
|
||||
<p className={`${bodyClass} mt-1 text-sm`}>{row.detail}</p>
|
||||
{t.platforms.map(([name, detail]) => (
|
||||
<section key={name} className="py-4 hairline-b">
|
||||
<h3 className="font-display text-lg">{name}</h3>
|
||||
<p className={`${t.bodyClassName} mt-1 text-sm`}>{detail}</p>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="policies" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "策略与回退" : "Policies and fallbacks"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh ? (
|
||||
<>
|
||||
本地 <code className="inline">sandbox_mode</code> 取值为{" "}
|
||||
<code className="inline">read-only</code>、<code className="inline">workspace-write</code>、
|
||||
<code className="inline">danger-full-access</code> 或{" "}
|
||||
<code className="inline">external-sandbox</code>。前两者只在选中且可用的 Seatbelt 或
|
||||
bubblewrap 包装器下被强制执行;<code className="inline">danger-full-access</code>{" "}
|
||||
有意绕过本地 OS 包装器;<code className="inline">external-sandbox</code>{" "}
|
||||
声明执行已被外部隔离。没有选中包装器时,shell 命令在没有 Codewhale OS 隔离的情况下运行——审批规则和感知工作区的原生文件工具仍是独立的控制。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The local <code className="inline">sandbox_mode</code> values are{" "}
|
||||
<code className="inline">read-only</code>, <code className="inline">workspace-write</code>,{" "}
|
||||
<code className="inline">danger-full-access</code>, and{" "}
|
||||
<code className="inline">external-sandbox</code>. The first two are enforced by Seatbelt or
|
||||
bubblewrap only when that wrapper is selected and available;{" "}
|
||||
<code className="inline">danger-full-access</code> deliberately bypasses the local OS
|
||||
wrapper; <code className="inline">external-sandbox</code> declares that execution is already
|
||||
externally isolated. When no wrapper is selected, the shell command runs without Codewhale
|
||||
OS isolation — approval rules and workspace-aware native file tools remain separate controls.
|
||||
</>
|
||||
<h2 className="font-display text-2xl mb-1">{t.policiesTitle}</h2>
|
||||
<p className={`${t.bodyClassName} mt-3`}>
|
||||
{splitTokens(t.policiesLead).map((part, i) =>
|
||||
"token" in part ? (
|
||||
<code key={`${i}-${part.token}`} className="inline">
|
||||
{CODE_SPANS[part.token] ?? `{${part.token}}`}
|
||||
</code>
|
||||
) : (
|
||||
<Fragment key={`${i}-text`}>{part.text}</Fragment>
|
||||
),
|
||||
)}
|
||||
</p>
|
||||
<pre className="code-block mt-4">{`# config.toml
|
||||
@@ -123,25 +70,13 @@ CODEWHALE_SANDBOX_API_KEY`}</pre>
|
||||
</section>
|
||||
|
||||
<section id="diagnostics" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "诊断与限制" : "Diagnostics and limits"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "codewhale setup --status、codewhale doctor、codewhale doctor --json 和 diagnostics 工具会报告应用 bubblewrap 偏好后本地可用的包装器。拒绝归因是保守的:子命令的通用 Permission denied 本身并不能证明是 Codewhale 的沙箱拦截了它,未沙箱化的命令失败永远不会被标记为沙箱拒绝。"
|
||||
: "codewhale setup --status, codewhale doctor, codewhale doctor --json, and the diagnostics tool report the locally available wrapper after applying the resolved bubblewrap preference. Denial attribution is intentionally conservative: a child command's generic Permission denied is not by itself proof that Codewhale's sandbox blocked it, and unsandboxed command failures are never labeled sandbox denials."}
|
||||
</p>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "限制同样如实说明:可用性在启动前检查,选中的包装器仍可能因主机策略、容器限制或竞态而失败;bubblewrap 会忽略缺失或不是目录的可写根;没有任何沙箱能防御内核漏洞或所有资源耗尽与侧信道攻击。"
|
||||
: "The limitations are stated just as plainly: availability is checked before launch, yet the selected wrapper can still fail because of host policy, container restrictions, or a race after the probe; bubblewrap ignores a configured writable root that is missing or not a directory; and no sandbox protects against kernel vulnerabilities or all resource-exhaustion and side-channel attacks."}
|
||||
</p>
|
||||
<h2 className="font-display text-2xl mb-1">{t.diagnosticsTitle}</h2>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.diagnosticsLead}</p>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.diagnosticsLimits}</p>
|
||||
</section>
|
||||
|
||||
<section id="source" className="hairline-t pt-8">
|
||||
<p className="text-sm text-ink-mute">
|
||||
{isZh
|
||||
? "来源文档:docs/SANDBOX.md · 更新时请同步修改 docs-map.ts。"
|
||||
: "Source document: docs/SANDBOX.md · Update docs-map.ts when changing."}
|
||||
</p>
|
||||
<p className="text-sm text-ink-mute">{t.sourceNote}</p>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -1,109 +1,75 @@
|
||||
import { Fragment } from "react";
|
||||
import { getDocsWeb, splitTokens } from "@/lib/i18n/dictionaries";
|
||||
import { buildPageMetadata } from "@/lib/page-meta";
|
||||
|
||||
/**
|
||||
* The commands, flags and addresses the overview and local paragraphs typeset
|
||||
* as inline `<code>`. `docs/VOICE.md` keeps commands, flags and paths
|
||||
* code-owned, so the dictionaries carry `{token}`s rather than the literals.
|
||||
*/
|
||||
const CODE_SPANS: Record<string, string> = {
|
||||
webCommand: "codewhale web",
|
||||
loopbackHost: "127.0.0.1",
|
||||
defaultUrl: "http://127.0.0.1:7878",
|
||||
portExample: "codewhale web --port 8788",
|
||||
portFlag: "--port",
|
||||
hostFlag: "--host",
|
||||
mobileCommand: "codewhale app-server --mobile",
|
||||
httpFlag: "--http",
|
||||
};
|
||||
|
||||
function withCodeSpans(template: string) {
|
||||
return splitTokens(template).map((part, i) =>
|
||||
"token" in part ? (
|
||||
<code key={`${i}-${part.token}`} className="inline">
|
||||
{CODE_SPANS[part.token] ?? `{${part.token}}`}
|
||||
</code>
|
||||
) : (
|
||||
<Fragment key={`${i}-text`}>{part.text}</Fragment>
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const t = getDocsWeb(locale);
|
||||
return buildPageMetadata({
|
||||
path: "/docs/web",
|
||||
locale,
|
||||
title: isZh ? "浏览器客户端 · Codewhale 文档" : "Browser Client · Codewhale Docs",
|
||||
description: isZh
|
||||
? "仅回环的内嵌浏览器客户端:一次性引导、会话 Cookie 与本地信任边界。"
|
||||
: "The loopback-only embedded browser client: one-time bootstrap, session cookie, and the local trust boundary.",
|
||||
title: t.metaTitle,
|
||||
description: t.metaDescription,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function WebClientPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const bodyClass = isZh
|
||||
? "text-ink-soft leading-[1.9] tracking-wide"
|
||||
: "text-ink-soft leading-relaxed";
|
||||
const t = getDocsWeb(locale);
|
||||
|
||||
return (
|
||||
<section className="space-y-10">
|
||||
<section id="overview" className="scroll-mt-32">
|
||||
<h1 className="font-display text-3xl mb-1">{isZh ? "浏览器客户端" : "Browser Client"}</h1>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh ? (
|
||||
<>
|
||||
<code className="inline">codewhale web</code> 在 canonical 运行时 API
|
||||
之上打开 Codewhale 内嵌的浏览器客户端。它是一个纯本地界面:服务器始终绑定{" "}
|
||||
<code className="inline">127.0.0.1</code>,无法改绑到局域网地址,也无法在关闭运行时认证的情况下运行。默认地址是{" "}
|
||||
<code className="inline">http://127.0.0.1:7878</code>;端口冲突时用{" "}
|
||||
<code className="inline">codewhale web --port 8788</code>{" "}
|
||||
换一个回环端口。Ctrl+C 停止进程,浏览器会话随之结束。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<code className="inline">codewhale web</code> opens Codewhale's embedded browser client
|
||||
over the canonical Runtime API. It is a local surface: the server always binds to{" "}
|
||||
<code className="inline">127.0.0.1</code>, cannot be rebound to a LAN address, and cannot
|
||||
run with Runtime authentication disabled. The default address is{" "}
|
||||
<code className="inline">http://127.0.0.1:7878</code>; on a port collision, pick another
|
||||
loopback port with <code className="inline">codewhale web --port 8788</code>. Stop the
|
||||
process with Ctrl+C and the browser session ends with it.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "当前客户端提供响应式的线程与搜索侧栏、由运行时持有的会话事实、transcript 与工具收据,以及输入区。它可以创建、选择、重命名和归档线程;发起或引导回合;中断工作;处理审批;回答运行时的用户输入请求。浏览器只是同一个本地运行时的另一视图——不会创建第二个云账号,不会把 provider 凭据复制进浏览器存储,也不会削弱已配置的审批与沙箱策略。"
|
||||
: "The current client provides a responsive thread and search rail, Runtime-owned session facts, transcript and tool receipts, and a composer. It can create, select, rename, and archive threads; start or steer turns; interrupt work; resolve approvals; and answer Runtime user-input requests. The browser is another view of the same local Runtime — it does not create a second cloud account, copy provider credentials into browser storage, or weaken the configured approval and sandbox policies."}
|
||||
</p>
|
||||
<h1 className="font-display text-3xl mb-1">{t.overviewTitle}</h1>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{withCodeSpans(t.overviewLead)}</p>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.overviewBody}</p>
|
||||
</section>
|
||||
|
||||
<section id="auth" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "认证边界" : "Authentication boundary"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "启动 URL 携带的是一个随机、短寿命、一次性的引导凭证——绝不是运行时 bearer 令牌。一次回环请求把它换成 HttpOnly、SameSite=Strict、进程本地的会话 Cookie,并立即使该凭证失效。重用、过期、畸形或非回环的引导尝试都会失败关闭。运行时令牌不会出现在渲染的 HTML、浏览器存储、URL 查询或片段、或浏览器启动参数中。携带 Cookie 的状态变更请求还必须出示精确的本地 web 源;跨源浏览器请求会被拒绝。"
|
||||
: "The browser-launch URL carries a random, short-lived, one-time bootstrap capability — never the Runtime bearer token. A loopback request exchanges it for an HttpOnly, SameSite=Strict, process-local session cookie and immediately invalidates the capability. Reused, expired, malformed, and non-loopback bootstrap attempts fail closed. The Runtime token is never placed in rendered HTML, browser storage, URL queries or fragments, or browser-launch arguments. Cookie-authenticated state-changing requests must also present the exact local web origin; cross-origin browser requests are rejected."}
|
||||
</p>
|
||||
<h2 className="font-display text-2xl mb-1">{t.authTitle}</h2>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.authLead}</p>
|
||||
</section>
|
||||
|
||||
<section id="local" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "本地就是本地" : "Local means local"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh ? (
|
||||
<>
|
||||
<code className="inline">codewhale web</code> 只接受{" "}
|
||||
<code className="inline">--port</code>——没有 <code className="inline">--host</code>
|
||||
,也没有关闭认证的选项。不要把它当公开网站,也不要通过路由器转发、公开反向代理或隧道暴露它的端口。单独的{" "}
|
||||
<code className="inline">codewhale app-server --mobile</code> 和{" "}
|
||||
<code className="inline">--http</code>{" "}
|
||||
模式有不同的部署与认证约定,操作它们(尤其是选择非回环绑定)之前请阅读运行时 API 文档。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<code className="inline">codewhale web</code> accepts only{" "}
|
||||
<code className="inline">--port</code> — there is no <code className="inline">--host</code>{" "}
|
||||
and no insecure-auth option on this command. Do not treat it as a public website or expose
|
||||
its port through router forwarding, a public reverse proxy, or a tunnel. The separate{" "}
|
||||
<code className="inline">codewhale app-server --mobile</code> and{" "}
|
||||
<code className="inline">--http</code> modes carry different deployment and authentication
|
||||
contracts; read the Runtime API documentation before operating either one, especially
|
||||
before selecting a non-loopback bind.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<h2 className="font-display text-2xl mb-1">{t.localTitle}</h2>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{withCodeSpans(t.localLead)}</p>
|
||||
</section>
|
||||
|
||||
<section id="troubleshooting" className="scroll-mt-32">
|
||||
<h2 className="font-display text-2xl mb-1">{isZh ? "常见问题" : "Troubleshooting"}</h2>
|
||||
<p className={`${bodyClass} mt-3`}>
|
||||
{isZh
|
||||
? "端口 7878 被占用时用 --port 换一个。浏览器无法打开时命令会报错退出,而不会留下可重用的引导凭证;检查系统默认浏览器设置后重新启动。页面能打开但 provider 不可用时,查 codewhale doctor 和 /provider——web 命令不配置也不迁移 provider 凭据。会话过期后重启 codewhale web 以签发新的进程本地会话;重用旧的引导 URL 本来就会失败。"
|
||||
: "If port 7878 is occupied, pass an unused --port. If the browser cannot be opened, the command exits with an error rather than leaving a reusable bootstrap capability behind; check the OS default-browser setup and start again. If the page loads but a provider is unavailable, inspect codewhale doctor and /provider — the web command does not configure or move provider credentials. If a session expired, restart codewhale web to mint a new process-local session; reusing an old bootstrap URL is expected to fail."}
|
||||
</p>
|
||||
<h2 className="font-display text-2xl mb-1">{t.troubleshootingTitle}</h2>
|
||||
<p className={`${t.bodyClassName} mt-3`}>{t.troubleshootingLead}</p>
|
||||
</section>
|
||||
|
||||
<section id="source" className="hairline-t pt-8">
|
||||
<p className="text-sm text-ink-mute">
|
||||
{isZh
|
||||
? "来源文档:docs/WEB.md · 更新时请同步修改 docs-map.ts。"
|
||||
: "Source document: docs/WEB.md · Update docs-map.ts when changing."}
|
||||
</p>
|
||||
<p className="text-sm text-ink-mute">{t.sourceNote}</p>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
EN_DOCS_CONSTITUTION,
|
||||
EN_DOCS_HOOKS,
|
||||
EN_DOCS_RUNTIME_API,
|
||||
EN_DOCS_SANDBOX,
|
||||
EN_DOCS_WEB,
|
||||
EN_DOCS_SHELL,
|
||||
EN_DOCS_TROUBLESHOOTING,
|
||||
EN_HOME,
|
||||
@@ -15,6 +17,8 @@ import {
|
||||
getDocsConstitution,
|
||||
getDocsHooks,
|
||||
getDocsRuntimeApi,
|
||||
getDocsSandbox,
|
||||
getDocsWeb,
|
||||
getDocsShell,
|
||||
getDocsTroubleshooting,
|
||||
getHome,
|
||||
@@ -247,6 +251,8 @@ describe("website dictionaries", () => {
|
||||
["docs-troubleshooting", getDocsTroubleshooting, EN_DOCS_TROUBLESHOOTING],
|
||||
["docs-constitution", getDocsConstitution, EN_DOCS_CONSTITUTION],
|
||||
["docs-runtime-api", getDocsRuntimeApi, EN_DOCS_RUNTIME_API],
|
||||
["docs-sandbox", getDocsSandbox, EN_DOCS_SANDBOX],
|
||||
["docs-web", getDocsWeb, EN_DOCS_WEB],
|
||||
] as const) {
|
||||
const enKeys = Object.keys(reference).sort();
|
||||
for (const locale of [...DICTIONARY_LOCALES, "fr", "und"]) {
|
||||
@@ -277,6 +283,9 @@ describe("website dictionaries", () => {
|
||||
getDocsRuntimeApi(locale).entries.map(([key]) => key),
|
||||
`${locale} runtime entries`,
|
||||
).toEqual(["http", "mobile", "stdio", "web", "doctor", "acp", "exec"]);
|
||||
// The platform rows are keyed by their own translated name rather than a
|
||||
// code-owned key, so only the count is comparable across locales.
|
||||
expect(getDocsSandbox(locale).platforms, `${locale} sandbox platforms`).toHaveLength(4);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -317,6 +326,38 @@ describe("website dictionaries", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("carries every code-span token through the sandbox and web copy", () => {
|
||||
const tokensOf = (template: string) =>
|
||||
splitTokens(template).flatMap((part) => ("token" in part ? [part.token] : []));
|
||||
for (const locale of [...DICTIONARY_LOCALES, "und"]) {
|
||||
// Two of these repeat, and the order is the sentence's, so this is a
|
||||
// stricter check than check-locales.mjs, which compares token sets.
|
||||
expect(tokensOf(getDocsSandbox(locale).policiesLead), `${locale} policiesLead`).toEqual([
|
||||
"sandboxMode",
|
||||
"readOnly",
|
||||
"workspaceWrite",
|
||||
"dangerFullAccess",
|
||||
"externalSandbox",
|
||||
"dangerFullAccess",
|
||||
"externalSandbox",
|
||||
]);
|
||||
const web = getDocsWeb(locale);
|
||||
expect(tokensOf(web.overviewLead), `${locale} web overviewLead`).toEqual([
|
||||
"webCommand",
|
||||
"loopbackHost",
|
||||
"defaultUrl",
|
||||
"portExample",
|
||||
]);
|
||||
expect(tokensOf(web.localLead), `${locale} localLead`).toEqual([
|
||||
"webCommand",
|
||||
"portFlag",
|
||||
"hostFlag",
|
||||
"mobileCommand",
|
||||
"httpFlag",
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it("splitTokens interleaves literal text and token names in template order", () => {
|
||||
expect(splitTokens("a {one} b {two}")).toEqual([
|
||||
{ text: "a " },
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { DocsSandboxDict } from "../types";
|
||||
|
||||
/**
|
||||
* English reference dictionary for `app/[locale]/docs/sandbox/page.tsx`.
|
||||
* Copy moved verbatim from the page's `isZh` ternaries — any wording change
|
||||
* belongs in its own commit, never mixed into a structural move.
|
||||
*/
|
||||
export const docsSandbox: DocsSandboxDict = {
|
||||
metaTitle: "Sandbox & Approval · Codewhale Docs",
|
||||
metaDescription:
|
||||
"The honest boundary: macOS Seatbelt, opt-in Linux bubblewrap, platform gaps, and approval policy.",
|
||||
bodyClassName: "text-ink-soft leading-relaxed",
|
||||
overviewTitle: "Sandbox & Approval",
|
||||
overviewLead:
|
||||
"Codewhale can launch shell commands proposed by a model. Approval policy, workspace-aware tools, and an operating-system command wrapper are separate controls: an approval is not a sandbox, and selecting workspace-write does not prove the current platform has an OS wrapper available. This page describes only behavior wired into the command execution path.",
|
||||
platforms: [
|
||||
[
|
||||
"macOS · Seatbelt",
|
||||
"Codewhale probes /usr/bin/sandbox-exec; when the probe succeeds and the policy requests a sandbox, the child command is wrapped in a generated Seatbelt profile: broad filesystem reads, policy-limited writes, and network only when the policy enables it. A failed probe is reported honestly as no OS sandbox.",
|
||||
],
|
||||
[
|
||||
"Linux · opt-in bubblewrap",
|
||||
"Linux command sandboxing is opt-in: set prefer_bwrap = true and keep /usr/bin/bwrap executable. The child gets a read-only root view with writable mounts derived from the resolved policy; the network namespace is isolated by default and --share-net is added only when the policy enables network access. Without the opt-in, Codewhale reports none.",
|
||||
],
|
||||
[
|
||||
"Windows · no OS sandbox",
|
||||
"The Windows command path currently reports no OS sandbox. Host permissions and approval policy still apply, but they are not a Codewhale OS command sandbox.",
|
||||
],
|
||||
[
|
||||
"External OpenSandbox execution",
|
||||
'With sandbox_backend = "opensandbox", shell execution is sent to the configured OpenSandbox-compatible HTTP endpoint instead of starting a local child. Isolation guarantees belong to the configured service and its operator.',
|
||||
],
|
||||
],
|
||||
policiesTitle: "Policies and fallbacks",
|
||||
policiesLead:
|
||||
"The local {sandboxMode} values are {readOnly}, {workspaceWrite}, {dangerFullAccess}, and {externalSandbox}. The first two are enforced by Seatbelt or bubblewrap only when that wrapper is selected and available; {dangerFullAccess} deliberately bypasses the local OS wrapper; {externalSandbox} declares that execution is already externally isolated. When no wrapper is selected, the shell command runs without Codewhale OS isolation — approval rules and workspace-aware native file tools remain separate controls.",
|
||||
diagnosticsTitle: "Diagnostics and limits",
|
||||
diagnosticsLead:
|
||||
"codewhale setup --status, codewhale doctor, codewhale doctor --json, and the diagnostics tool report the locally available wrapper after applying the resolved bubblewrap preference. Denial attribution is intentionally conservative: a child command's generic Permission denied is not by itself proof that Codewhale's sandbox blocked it, and unsandboxed command failures are never labeled sandbox denials.",
|
||||
diagnosticsLimits:
|
||||
"The limitations are stated just as plainly: availability is checked before launch, yet the selected wrapper can still fail because of host policy, container restrictions, or a race after the probe; bubblewrap ignores a configured writable root that is missing or not a directory; and no sandbox protects against kernel vulnerabilities or all resource-exhaustion and side-channel attacks.",
|
||||
sourceNote: "Source document: docs/SANDBOX.md · Update docs-map.ts when changing.",
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { DocsWebDict } from "../types";
|
||||
|
||||
/**
|
||||
* English reference dictionary for `app/[locale]/docs/web/page.tsx`.
|
||||
* Copy moved verbatim from the page's `isZh` ternaries — any wording change
|
||||
* belongs in its own commit, never mixed into a structural move.
|
||||
*/
|
||||
export const docsWeb: DocsWebDict = {
|
||||
metaTitle: "Browser Client · Codewhale Docs",
|
||||
metaDescription:
|
||||
"The loopback-only embedded browser client: one-time bootstrap, session cookie, and the local trust boundary.",
|
||||
bodyClassName: "text-ink-soft leading-relaxed",
|
||||
overviewTitle: "Browser Client",
|
||||
overviewLead:
|
||||
"{webCommand} opens Codewhale's embedded browser client over the canonical Runtime API. It is a local surface: the server always binds to {loopbackHost}, cannot be rebound to a LAN address, and cannot run with Runtime authentication disabled. The default address is {defaultUrl}; on a port collision, pick another loopback port with {portExample}. Stop the process with Ctrl+C and the browser session ends with it.",
|
||||
overviewBody:
|
||||
"The current client provides a responsive thread and search rail, Runtime-owned session facts, transcript and tool receipts, and a composer. It can create, select, rename, and archive threads; start or steer turns; interrupt work; resolve approvals; and answer Runtime user-input requests. The browser is another view of the same local Runtime — it does not create a second cloud account, copy provider credentials into browser storage, or weaken the configured approval and sandbox policies.",
|
||||
authTitle: "Authentication boundary",
|
||||
authLead:
|
||||
"The browser-launch URL carries a random, short-lived, one-time bootstrap capability — never the Runtime bearer token. A loopback request exchanges it for an HttpOnly, SameSite=Strict, process-local session cookie and immediately invalidates the capability. Reused, expired, malformed, and non-loopback bootstrap attempts fail closed. The Runtime token is never placed in rendered HTML, browser storage, URL queries or fragments, or browser-launch arguments. Cookie-authenticated state-changing requests must also present the exact local web origin; cross-origin browser requests are rejected.",
|
||||
localTitle: "Local means local",
|
||||
localLead:
|
||||
"{webCommand} accepts only {portFlag} — there is no {hostFlag} and no insecure-auth option on this command. Do not treat it as a public website or expose its port through router forwarding, a public reverse proxy, or a tunnel. The separate {mobileCommand} and {httpFlag} modes carry different deployment and authentication contracts; read the Runtime API documentation before operating either one, especially before selecting a non-loopback bind.",
|
||||
troubleshootingTitle: "Troubleshooting",
|
||||
troubleshootingLead:
|
||||
"If port 7878 is occupied, pass an unused --port. If the browser cannot be opened, the command exits with an error rather than leaving a reusable bootstrap capability behind; check the OS default-browser setup and start again. If the page loads but a provider is unavailable, inspect codewhale doctor and /provider — the web command does not configure or move provider credentials. If a session expired, restart codewhale web to mint a new process-local session; reusing an old bootstrap URL is expected to fail.",
|
||||
sourceNote: "Source document: docs/WEB.md · Update docs-map.ts when changing.",
|
||||
};
|
||||
@@ -20,9 +20,11 @@ import type {
|
||||
DocsGuideDict,
|
||||
DocsHooksDict,
|
||||
DocsRuntimeApiDict,
|
||||
DocsSandboxDict,
|
||||
DocsShellDict,
|
||||
DocsModesDict,
|
||||
DocsTroubleshootingDict,
|
||||
DocsWebDict,
|
||||
HomeDict,
|
||||
} from "./types";
|
||||
import { chrome as enChrome } from "./en/chrome";
|
||||
@@ -45,6 +47,10 @@ import { docsModes as enDocsModes } from "./en/docs-modes";
|
||||
import { docsModes as zhDocsModes } from "./zh/docs-modes";
|
||||
import { docsRuntimeApi as enDocsRuntimeApi } from "./en/docs-runtime-api";
|
||||
import { docsRuntimeApi as zhDocsRuntimeApi } from "./zh/docs-runtime-api";
|
||||
import { docsSandbox as enDocsSandbox } from "./en/docs-sandbox";
|
||||
import { docsSandbox as zhDocsSandbox } from "./zh/docs-sandbox";
|
||||
import { docsWeb as enDocsWeb } from "./en/docs-web";
|
||||
import { docsWeb as zhDocsWeb } from "./zh/docs-web";
|
||||
import { chrome as zhChrome } from "./zh/chrome";
|
||||
import { home as zhHome } from "./zh/home";
|
||||
import { chrome as jaChrome } from "./ja/chrome";
|
||||
@@ -182,6 +188,14 @@ const DOCS_RUNTIME_API: Record<string, DocsRuntimeApiDict> = {
|
||||
zh: zhDocsRuntimeApi,
|
||||
};
|
||||
|
||||
const DOCS_SANDBOX: Record<string, DocsSandboxDict> = {
|
||||
zh: zhDocsSandbox,
|
||||
};
|
||||
|
||||
const DOCS_WEB: Record<string, DocsWebDict> = {
|
||||
zh: zhDocsWeb,
|
||||
};
|
||||
|
||||
export function getChrome(locale: string): ChromeDict {
|
||||
return CHROME[locale] ?? enChrome;
|
||||
}
|
||||
@@ -226,6 +240,14 @@ export function getDocsRuntimeApi(locale: string): DocsRuntimeApiDict {
|
||||
return DOCS_RUNTIME_API[locale] ?? enDocsRuntimeApi;
|
||||
}
|
||||
|
||||
export function getDocsSandbox(locale: string): DocsSandboxDict {
|
||||
return DOCS_SANDBOX[locale] ?? enDocsSandbox;
|
||||
}
|
||||
|
||||
export function getDocsWeb(locale: string): DocsWebDict {
|
||||
return DOCS_WEB[locale] ?? enDocsWeb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select one side of a legacy `{ en, zh }` content pair by locale. This is
|
||||
* the transitional bridge for `web/lib/content/` modules that still carry
|
||||
@@ -249,6 +271,8 @@ export const EN_DOCS_CONSTITUTION = enDocsConstitution;
|
||||
export const EN_DOCS_FLEET = enDocsFleet;
|
||||
export const EN_DOCS_MODES = enDocsModes;
|
||||
export const EN_DOCS_RUNTIME_API = enDocsRuntimeApi;
|
||||
export const EN_DOCS_SANDBOX = enDocsSandbox;
|
||||
export const EN_DOCS_WEB = enDocsWeb;
|
||||
|
||||
/** Interpolate `{name}` tokens in a dictionary template. Unknown tokens are
|
||||
* left intact so a template/variable drift is visible in review, not silent. */
|
||||
|
||||
@@ -463,3 +463,35 @@ export interface DocsRuntimeApiDict {
|
||||
securityLead: string;
|
||||
sourceNote: string;
|
||||
}
|
||||
|
||||
export interface DocsSandboxDict {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
bodyClassName: string;
|
||||
overviewTitle: string;
|
||||
overviewLead: string;
|
||||
/** Four `[name, detail]` rows; the name is also the row's key. */
|
||||
platforms: [string, string][];
|
||||
policiesTitle: string;
|
||||
policiesLead: string;
|
||||
diagnosticsTitle: string;
|
||||
diagnosticsLead: string;
|
||||
diagnosticsLimits: string;
|
||||
sourceNote: string;
|
||||
}
|
||||
|
||||
export interface DocsWebDict {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
bodyClassName: string;
|
||||
overviewTitle: string;
|
||||
overviewLead: string;
|
||||
overviewBody: string;
|
||||
authTitle: string;
|
||||
authLead: string;
|
||||
localTitle: string;
|
||||
localLead: string;
|
||||
troubleshootingTitle: string;
|
||||
troubleshootingLead: string;
|
||||
sourceNote: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { DocsSandboxDict } from "../types";
|
||||
|
||||
/** 中文对照见 `en/docs-sandbox.ts`,文案自页面的 `isZh` 三元逐字迁入。 */
|
||||
export const docsSandbox: DocsSandboxDict = {
|
||||
metaTitle: "沙箱与审批 · Codewhale 文档",
|
||||
metaDescription: "macOS Seatbelt、Linux 可选 bubblewrap、平台缺口和审批策略的真实边界。",
|
||||
bodyClassName: "text-ink-soft leading-[1.9] tracking-wide",
|
||||
overviewTitle: "沙箱与审批",
|
||||
overviewLead:
|
||||
"Codewhale 可以启动由模型提出的 shell 命令。审批策略、感知工作区的文件工具和操作系统命令包装器是三个独立的控制:一次审批不是沙箱,选择 workspace-write 也不代表当前平台有可用的 OS 包装器。本页只描述已经接入命令执行路径的行为。",
|
||||
platforms: [
|
||||
[
|
||||
"macOS · Seatbelt",
|
||||
"Codewhale 探测 /usr/bin/sandbox-exec;探测成功且策略要求沙箱时,子命令会被包上运行时生成的 Seatbelt profile:广泛的文件系统读取、按策略限制的写入、仅在策略允许时放行网络。探测失败则如实报告无 OS 沙箱。",
|
||||
],
|
||||
[
|
||||
"Linux · 可选 bubblewrap",
|
||||
"Linux 命令沙箱是显式启用的:设置 prefer_bwrap = true,且 /usr/bin/bwrap 是可执行文件时才选用。子命令得到只读根视图,writable 挂载来自解析后的策略;默认隔离网络命名空间,仅在策略开启 network_access 时加 --share-net。未启用或未安装 bwrap 时报告 none。",
|
||||
],
|
||||
[
|
||||
"Windows · 无 OS 沙箱",
|
||||
"Windows 命令路径目前报告无 OS 沙箱。主机权限和审批策略仍然有效,但它们不是 Codewhale 的 OS 命令沙箱。",
|
||||
],
|
||||
[
|
||||
"外部 OpenSandbox 执行",
|
||||
'配置 sandbox_backend = "opensandbox" 后,shell 执行会发往配置的 OpenSandbox 兼容 HTTP 端点,而不是启动本地子进程。隔离保证属于所配置的服务及其运营者。',
|
||||
],
|
||||
],
|
||||
policiesTitle: "策略与回退",
|
||||
policiesLead:
|
||||
"本地 {sandboxMode} 取值为 {readOnly}、{workspaceWrite}、{dangerFullAccess} 或 {externalSandbox}。前两者只在选中且可用的 Seatbelt 或 bubblewrap 包装器下被强制执行;{dangerFullAccess} 有意绕过本地 OS 包装器;{externalSandbox} 声明执行已被外部隔离。没有选中包装器时,shell 命令在没有 Codewhale OS 隔离的情况下运行——审批规则和感知工作区的原生文件工具仍是独立的控制。",
|
||||
diagnosticsTitle: "诊断与限制",
|
||||
diagnosticsLead:
|
||||
"codewhale setup --status、codewhale doctor、codewhale doctor --json 和 diagnostics 工具会报告应用 bubblewrap 偏好后本地可用的包装器。拒绝归因是保守的:子命令的通用 Permission denied 本身并不能证明是 Codewhale 的沙箱拦截了它,未沙箱化的命令失败永远不会被标记为沙箱拒绝。",
|
||||
diagnosticsLimits:
|
||||
"限制同样如实说明:可用性在启动前检查,选中的包装器仍可能因主机策略、容器限制或竞态而失败;bubblewrap 会忽略缺失或不是目录的可写根;没有任何沙箱能防御内核漏洞或所有资源耗尽与侧信道攻击。",
|
||||
sourceNote: "来源文档:docs/SANDBOX.md · 更新时请同步修改 docs-map.ts。",
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { DocsWebDict } from "../types";
|
||||
|
||||
/** 中文对照见 `en/docs-web.ts`,文案自页面的 `isZh` 三元逐字迁入。 */
|
||||
export const docsWeb: DocsWebDict = {
|
||||
metaTitle: "浏览器客户端 · Codewhale 文档",
|
||||
metaDescription: "仅回环的内嵌浏览器客户端:一次性引导、会话 Cookie 与本地信任边界。",
|
||||
bodyClassName: "text-ink-soft leading-[1.9] tracking-wide",
|
||||
overviewTitle: "浏览器客户端",
|
||||
overviewLead:
|
||||
"{webCommand} 在 canonical 运行时 API 之上打开 Codewhale 内嵌的浏览器客户端。它是一个纯本地界面:服务器始终绑定 {loopbackHost},无法改绑到局域网地址,也无法在关闭运行时认证的情况下运行。默认地址是 {defaultUrl};端口冲突时用 {portExample} 换一个回环端口。Ctrl+C 停止进程,浏览器会话随之结束。",
|
||||
overviewBody:
|
||||
"当前客户端提供响应式的线程与搜索侧栏、由运行时持有的会话事实、transcript 与工具收据,以及输入区。它可以创建、选择、重命名和归档线程;发起或引导回合;中断工作;处理审批;回答运行时的用户输入请求。浏览器只是同一个本地运行时的另一视图——不会创建第二个云账号,不会把 provider 凭据复制进浏览器存储,也不会削弱已配置的审批与沙箱策略。",
|
||||
authTitle: "认证边界",
|
||||
authLead:
|
||||
"启动 URL 携带的是一个随机、短寿命、一次性的引导凭证——绝不是运行时 bearer 令牌。一次回环请求把它换成 HttpOnly、SameSite=Strict、进程本地的会话 Cookie,并立即使该凭证失效。重用、过期、畸形或非回环的引导尝试都会失败关闭。运行时令牌不会出现在渲染的 HTML、浏览器存储、URL 查询或片段、或浏览器启动参数中。携带 Cookie 的状态变更请求还必须出示精确的本地 web 源;跨源浏览器请求会被拒绝。",
|
||||
localTitle: "本地就是本地",
|
||||
localLead:
|
||||
"{webCommand} 只接受 {portFlag}——没有 {hostFlag},也没有关闭认证的选项。不要把它当公开网站,也不要通过路由器转发、公开反向代理或隧道暴露它的端口。单独的 {mobileCommand} 和 {httpFlag} 模式有不同的部署与认证约定,操作它们(尤其是选择非回环绑定)之前请阅读运行时 API 文档。",
|
||||
troubleshootingTitle: "常见问题",
|
||||
troubleshootingLead:
|
||||
"端口 7878 被占用时用 --port 换一个。浏览器无法打开时命令会报错退出,而不会留下可重用的引导凭证;检查系统默认浏览器设置后重新启动。页面能打开但 provider 不可用时,查 codewhale doctor 和 /provider——web 命令不配置也不迁移 provider 凭据。会话过期后重启 codewhale web 以签发新的进程本地会话;重用旧的引导 URL 本来就会失败。",
|
||||
sourceNote: "来源文档:docs/WEB.md · 更新时请同步修改 docs-map.ts。",
|
||||
};
|
||||
@@ -35,6 +35,8 @@ const OPTIONAL_FILES = [
|
||||
"docs-fleet.ts",
|
||||
"docs-modes.ts",
|
||||
"docs-runtime-api.ts",
|
||||
"docs-sandbox.ts",
|
||||
"docs-web.ts",
|
||||
];
|
||||
|
||||
/** Top-level keys of the exported object literal (two-space indented `key:`). */
|
||||
|
||||
Reference in New Issue
Block a user