-
-
Notifications
You must be signed in to change notification settings - Fork 203
[codex] Add OpenClaw security roadmap article #141
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
b42cab2
docs: add OpenClaw security roadmap article
steipete ce19294
docs: polish security roadmap article
steipete 21d5433
docs: link Proxyline site
steipete 065ce56
docs: add supporting docs links
steipete 05392b4
docs: tighten security roadmap article
steipete 28d1a6f
docs: make security article less outline-like
steipete 34f8cbc
docs: trim filesystem status line
steipete 9514765
docs: trim static analysis status line
steipete 9a32c34
fix: stop blog article background flicker
steipete b4b3cc5
docs: add security article imagery
steipete e790d7d
Revert "docs: add security article imagery"
steipete 620084d
docs: add security article evidence images
steipete c9778f7
docs: add Jesse Merhi X profile link
steipete f9c850f
docs(blog): trim and tighten 'Where OpenClaw Security Is Heading'
steipete 4a0bad7
docs(blog): solid article card, wider layout, rounded evidence figures
steipete 4df6c62
docs(blog): crop decorative outer frame from security evidence images
steipete File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+171 KB
...claw-security-is-heading/clawhub-malicious-install-blocked-compact-evidence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+561 KB
.../where-openclaw-security-is-heading/command-ast-approval-highlight-evidence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+156 KB
...c/blog/where-openclaw-security-is-heading/fs-safe-boundary-refusal-evidence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+197 KB
...og/where-openclaw-security-is-heading/opengrep-ghsa-local-rule-hit-evidence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+181 KB
...ic/blog/where-openclaw-security-is-heading/proxy-validation-egress-evidence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| --- | ||
| title: "Where OpenClaw Security Is Heading" | ||
| description: "The security roadmap for making OpenClaw a powerful personal assistant runtime users can understand, observe, and trust." | ||
| date: 2026-05-15 | ||
| authors: | ||
| - name: "Jesse Merhi" | ||
| links: | ||
| - label: "LinkedIn" | ||
| url: "https://www.linkedin.com/in/jesse-merhi/" | ||
| - label: "@jesse_merhi" | ||
| url: "https://x.com/jesse_merhi" | ||
| avatar: "/blog/authors/jesse-merhi.jpg" | ||
| draft: false | ||
| tags: ["security", "open-source", "clawhub", "plugins"] | ||
| --- | ||
|
|
||
| Our goal is for OpenClaw to become a trusted way to run a powerful AI personal assistant. | ||
|
|
||
| OpenClaw can read files, run commands, install plugins, talk to the network, and act on a real machine for a real user. Power like that is easy to describe as dangerous. The concern is fair. Powerful does not have to mean blind, unbounded, or impossible to audit. | ||
|
|
||
| Some of this has landed. Some is rolling out. Some is still in flight. Some is research. I want to be clear about the difference, because posts that blur those lines mislead readers. | ||
|
|
||
| ## Filesystem boundaries and fs-safe | ||
|
|
||
| OpenClaw runs on your machine. That means it can touch your documents, your codebases, and your photos. | ||
|
|
||
| The filesystem risk people usually reach for first is path traversal. That risk is real, but it is also only one symptom of a bigger class of bugs: unclear boundaries. Code thinks it is writing inside one root, then a symlink, absolute path, archive extraction, or sloppy join makes it cross another. | ||
|
|
||
| [`fs-safe`](https://fs-safe.io/) is one answer to that. It is the set of [safe filesystem patterns](https://docs.openclaw.ai/gateway/security/secure-file-operations) OpenClaw had already been growing, pulled into a shared library so core code, plugins, and adjacent services can use the same root-bounded primitives. | ||
|
|
||
| It is not a sandbox. A plugin that is allowed to run arbitrary shell commands can still do arbitrary shell-command things. `fs-safe` protects against boundary-crossing bugs in filesystem code. | ||
|
|
||
| Writing inside a plugin workspace should work. Traversal and absolute-path writes outside that workspace should fail. Plugin authors should not have to reimplement those checks. | ||
|
|
||
| <figure class="evidence-figure"> | ||
| <img src="/blog/where-openclaw-security-is-heading/fs-safe-boundary-refusal-evidence.png" alt="Terminal output showing fs-safe allowing an in-workspace write and blocking traversal and absolute-path writes with outside-workspace errors." loading="lazy" /> | ||
| <figcaption>Writing inside the plugin workspace succeeds. Traversal and absolute-path writes are refused as <code>outside-workspace</code>.</figcaption> | ||
| </figure> | ||
|
|
||
| The next step is making these primitives the expected pattern for plugins on ClawHub too. Bypassing them is not automatically malicious, but it is security-relevant. Over time, that kind of choice should count against a plugin's trust posture. | ||
|
|
||
| The safest filesystem call is still the one we do not make. That is the security motivation behind the in-flight SQLite runtime-state refactor. Sessions, transcripts, scheduler state, and plugin state belong in a typed database with clear ownership and transactions, not loose files. Moving runtime state into SQLite removes whole categories of filesystem access from the runtime path. | ||
|
|
||
| ## Network egress and Proxyline | ||
|
|
||
| Agentic systems make SSRF harder than it is in a normal web service. In a normal service, user-controlled URLs are often the exception. In an agent runtime, user-controlled or model-influenced URLs are normal product behavior. "Fetch this URL because someone, or something, asked for it" is normal work. | ||
|
|
||
| We started with the obvious approach: validate the URL before fetching it. That is not enough. Validation resolves DNS, the fetch resolves DNS again, and the answer can change between the two. A host that pointed at a public IP during validation can point at a metadata endpoint by the time the request leaves. | ||
|
|
||
| The fix has to move closer to egress. | ||
|
|
||
| [Proxyline](https://proxyline.dev/) is our Node-process routing layer for that. It installs process-global routing for Node networking surfaces and sends traffic through the [proxy you configured](https://docs.openclaw.ai/security/network-proxy). The configured proxy is where the connect-time policy should live: block metadata addresses, private ranges, loopback canaries, and whatever else your environment needs blocked. | ||
|
|
||
| Proxyline routes. The proxy enforces. | ||
|
|
||
| It also gives operators observability. If you already run a managed proxy, you can route OpenClaw through it and watch destinations, rates, and blocked attempts from infrastructure you already trust. | ||
|
|
||
| Proxyline is not a perfect cage around every possible byte. Raw sockets, native modules, unusual transports, early-captured agents, and non-OpenClaw child processes can still bypass a Node-level guardrail. But for ordinary OpenClaw network paths, moving the control point from "a wrapper remembered to validate this URL" to "egress flows through a proxy policy" is a much better shape. | ||
|
|
||
| The validation path is simple: `example.com` should pass, a loopback canary should fail, and [`openclaw proxy validate`](https://docs.openclaw.ai/cli/proxy) should prove the configured route behaves that way. | ||
|
|
||
| <figure class="evidence-figure"> | ||
| <img src="/blog/where-openclaw-security-is-heading/proxy-validation-egress-evidence.png" alt="Terminal output showing openclaw proxy validate allowing example.com, denying a loopback canary, and passing validation." loading="lazy" /> | ||
| <figcaption>The local filtering proxy allows <code>example.com</code>, blocks the loopback canary, and <code>openclaw proxy validate</code> passes.</figcaption> | ||
| </figure> | ||
|
|
||
| ## Plugin trust on ClawHub | ||
|
|
||
| ClawHub has to be the authority for plugin trust and provenance when a plugin comes from ClawHub. OpenClaw should consume those signals during install and update, rather than rely only on local inspection after the fact. | ||
|
|
||
| The ClawHub pipeline is a mix of signals: ClawScan, VirusTotal, static analysis, metadata checks, source provenance, and manual moderation. None of those is magic. Scanners are noisy in different ways, and a pipeline that screams about everything teaches users to ignore it. | ||
|
|
||
| That is where [ClawHub](https://docs.openclaw.ai/clawhub) can do something a local install flow cannot. It can attach [trust evidence](https://docs.openclaw.ai/clawhub/security-audits) to a specific package version. It can say this release is clean, suspicious, held, quarantined, revoked, or malicious. It can block downloads for [malicious or quarantined releases](https://docs.openclaw.ai/clawhub/moderation). It can show users what changed and why. | ||
|
|
||
| [Plugins can come from](https://docs.openclaw.ai/cli/plugins) GitHub, a private registry, or a file someone sends you. That is not going away, and OpenClaw should not pretend users do not own their own machines. | ||
|
|
||
| What we can do is make the safe path better. Publish on ClawHub. Get scanned. Attach evidence. Let users weigh that evidence before install. | ||
|
|
||
| We are also exploring higher-trust tiers above the baseline: official packages, trusted publishers, and packages held to stricter review expectations. For plugins that live outside ClawHub, we want scanning to reach them too, but the exact product shape still needs work. | ||
|
|
||
| If ClawHub marks a release as malicious and quarantined, the ClawHub install path should refuse it. That is the bar. | ||
|
|
||
| <figure class="evidence-figure"> | ||
| <img src="/blog/where-openclaw-security-is-heading/clawhub-malicious-install-blocked-compact-evidence.png" alt="Terminal output showing OpenClaw refusing to install a ClawHub release flagged as malicious and quarantined." loading="lazy" /> | ||
| <figcaption>ClawHub marks a release as malicious and quarantined; OpenClaw refuses the install.</figcaption> | ||
| </figure> | ||
|
|
||
| ## Command approvals and prompt fatigue | ||
|
|
||
| Prompts arrive faster than anyone can read them. After a few minutes, users flip on YOLO mode so work can continue. At that point the prompts train the user to stop reading. | ||
|
|
||
| Fixing this means fewer prompts, and better prompts. | ||
|
|
||
| The accuracy part starts with parsing. String matching is not enough. If an allowlist or blocklist only sees the outer command, wrappers become a bypass. A policy that understands `rm` but cannot see inside `bash -c "rm -rf ~/something"` is not a policy users should trust. | ||
|
|
||
| The [shell approval path](https://docs.openclaw.ai/tools/exec-approvals) now evaluates inner command chains for common shell `-c` wrappers. If the inner chain contains an executable that is not allowed, the wrapper should not make it safe. The command highlighter also uses Tree-sitter to surface what OpenClaw found inside wrappers. | ||
|
|
||
| <figure class="evidence-figure"> | ||
| <img src="/blog/where-openclaw-security-is-heading/command-ast-approval-highlight-evidence.png" alt="OpenClaw exec approval dialog highlighting executables inside a nested bash and Python command, including rm." loading="lazy" /> | ||
| <figcaption>The command highlighter identifies executables inside wrapper payloads, including the inner destructive command.</figcaption> | ||
| </figure> | ||
|
|
||
| PowerShell has its own traps; we fail closed for forms we do not understand, and broader support is on the roadmap. | ||
|
|
||
| Parsing is the easier half. The harder half is deciding when to ask. | ||
|
|
||
| A static approval policy either prompts on everything that might be risky, or relies on a fixed allow/deny list that cannot tell whether a command fits the current task. | ||
|
|
||
| The question users actually care about: did I want this to happen? | ||
|
|
||
| That is why we are experimenting with contextual approval. The goal is not "never prompt." The goal is that prompts mean something — and when they do, the user should stop and read. | ||
|
|
||
| For OpenAI users we expose [Auto Review](https://developers.openai.com/codex/concepts/sandboxing/auto-review), a codex-specific feature which replaces manual approval at the sandbox boundary with a separate reviewer agent. | ||
|
|
||
| ## Static analysis | ||
|
|
||
| OpenClaw has had a lot of GitHub Security Advisories. The first job was plugging holes. The next job is making sure the same bug class does not come back. | ||
|
|
||
| After an advisory is patched, it is tempting to call it done. A GHSA is evidence about a bug class, not just one bug. The question after triage is: can we find all the code that looks like this? | ||
|
|
||
| For that, we use OpenGrep with a precise rulepack. Each rule is tied to an advisory, report, or review finding. The baseline goal is regression detection: if the same vulnerable shape returns, CI catches it before review does. The better goal is variant detection: catch nearby versions of the same mistake. | ||
|
|
||
| Precision is everything. A noisy rule is worse than no rule, because it teaches the team to ignore the channel. | ||
|
|
||
| Today the checked-in precise OpenGrep rulepack has 148 rules. It runs on PR diffs, and the full scan can be run manually. New patched advisories become candidates for new rules. | ||
|
|
||
| <figure class="evidence-figure"> | ||
| <img src="/blog/where-openclaw-security-is-heading/opengrep-ghsa-local-rule-hit-evidence.png" alt="Terminal output showing an OpenGrep rule finding a GHSA-derived unsafe safe-bin profile fallback pattern." loading="lazy" /> | ||
| <figcaption>An OpenGrep rule catching a previous GHSA-shaped bug locally.</figcaption> | ||
| </figure> | ||
|
|
||
| CodeQL runs alongside for deeper semantic coverage. It is slower and noisier to maintain; we use both. | ||
|
|
||
| ## What This Means for OpenClaw Users | ||
|
|
||
| OpenClaw is not becoming less powerful. We are making the boundaries easier to see and defend. | ||
|
|
||
| We are not going to promise risk-free agents. Anyone promising that is selling something, or has not shipped enough yet. | ||
|
|
||
| What we can promise is the direction. OpenClaw can stay powerful while becoming more defensible. That is what we are building. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,13 +15,33 @@ const { post } = Astro.props; | |||||||||||||||||||||||||||||||||||||
| const { Content } = await render(post); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Normalize authors - support both legacy single author and new multi-author format | ||||||||||||||||||||||||||||||||||||||
| type Author = { name: string; handle: string }; | ||||||||||||||||||||||||||||||||||||||
| type AuthorLink = { label: string; url: string }; | ||||||||||||||||||||||||||||||||||||||
| type Author = { name: string; handle?: string; url?: string; links?: AuthorLink[]; avatar?: string }; | ||||||||||||||||||||||||||||||||||||||
| const authors: Author[] = post.data.authors | ||||||||||||||||||||||||||||||||||||||
| ? post.data.authors | ||||||||||||||||||||||||||||||||||||||
| : post.data.author && post.data.authorHandle | ||||||||||||||||||||||||||||||||||||||
| ? [{ name: post.data.author, handle: post.data.authorHandle }] | ||||||||||||||||||||||||||||||||||||||
| : post.data.author | ||||||||||||||||||||||||||||||||||||||
| ? [{ | ||||||||||||||||||||||||||||||||||||||
| name: post.data.author, | ||||||||||||||||||||||||||||||||||||||
| handle: post.data.authorHandle, | ||||||||||||||||||||||||||||||||||||||
| url: post.data.authorUrl, | ||||||||||||||||||||||||||||||||||||||
| avatar: post.data.authorAvatar, | ||||||||||||||||||||||||||||||||||||||
| }] | ||||||||||||||||||||||||||||||||||||||
| : []; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function getAuthorAvatar(author: Author): string { | ||||||||||||||||||||||||||||||||||||||
| return author.avatar ?? (author.handle ? `https://unavatar.io/x/${author.handle}` : '/openclaw-logo-text-dark.png'); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function getAuthorLinks(author: Author): AuthorLink[] { | ||||||||||||||||||||||||||||||||||||||
| if (author.links?.length) return author.links; | ||||||||||||||||||||||||||||||||||||||
| const url = author.url ?? (author.handle ? `https://x.com/${author.handle}` : null); | ||||||||||||||||||||||||||||||||||||||
| if (!url) return []; | ||||||||||||||||||||||||||||||||||||||
| return [{ | ||||||||||||||||||||||||||||||||||||||
| label: author.handle ? `@${author.handle}` : new URL(url).hostname.replace(/^www\./, ''), | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Missing error handling in getAuthorLinks function causes runtime TypeError when author.url is not a valid URL string |
||||||||||||||||||||||||||||||||||||||
| url, | ||||||||||||||||||||||||||||||||||||||
| }]; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function formatDate(date: Date): string { | ||||||||||||||||||||||||||||||||||||||
| return date.toLocaleDateString('en-US', { | ||||||||||||||||||||||||||||||||||||||
| year: 'numeric', | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -61,14 +81,20 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| {authors.map((author) => ( | ||||||||||||||||||||||||||||||||||||||
| <div class="author-info"> | ||||||||||||||||||||||||||||||||||||||
| <img | ||||||||||||||||||||||||||||||||||||||
| src={`https://unavatar.io/x/${author.handle}`} | ||||||||||||||||||||||||||||||||||||||
| src={getAuthorAvatar(author)} | ||||||||||||||||||||||||||||||||||||||
| alt={author.name} | ||||||||||||||||||||||||||||||||||||||
| class="author-avatar" | ||||||||||||||||||||||||||||||||||||||
| loading="lazy" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| <div class="author-details"> | ||||||||||||||||||||||||||||||||||||||
| <span class="author-name">{author.name}</span> | ||||||||||||||||||||||||||||||||||||||
| <a href={`https://x.com/${author.handle}`} class="author-handle" target="_blank" rel="noopener">@{author.handle}</a> | ||||||||||||||||||||||||||||||||||||||
| {getAuthorLinks(author).length > 0 && ( | ||||||||||||||||||||||||||||||||||||||
| <div class="author-links"> | ||||||||||||||||||||||||||||||||||||||
| {getAuthorLinks(author).map((link) => ( | ||||||||||||||||||||||||||||||||||||||
| <a href={link.url} class="author-handle" target="_blank" rel="noopener">{link.label}</a> | ||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -142,16 +168,10 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| radial-gradient(1px 1px at 250px 150px, rgba(255,255,255,0.5), transparent), | ||||||||||||||||||||||||||||||||||||||
| radial-gradient(2px 2px at 300px 40px, rgba(255,77,77,0.4), transparent); | ||||||||||||||||||||||||||||||||||||||
| background-size: 350px 200px; | ||||||||||||||||||||||||||||||||||||||
| animation: twinkle 8s ease-in-out infinite alternate; | ||||||||||||||||||||||||||||||||||||||
| pointer-events: none; | ||||||||||||||||||||||||||||||||||||||
| z-index: 0; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @keyframes twinkle { | ||||||||||||||||||||||||||||||||||||||
| 0% { opacity: 0.4; } | ||||||||||||||||||||||||||||||||||||||
| 100% { opacity: 0.7; } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .nebula { | ||||||||||||||||||||||||||||||||||||||
| position: fixed; | ||||||||||||||||||||||||||||||||||||||
| inset: 0; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -166,7 +186,7 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| .container { | ||||||||||||||||||||||||||||||||||||||
| position: relative; | ||||||||||||||||||||||||||||||||||||||
| z-index: 1; | ||||||||||||||||||||||||||||||||||||||
| max-width: 720px; | ||||||||||||||||||||||||||||||||||||||
| max-width: 792px; | ||||||||||||||||||||||||||||||||||||||
| margin: 0 auto; | ||||||||||||||||||||||||||||||||||||||
| padding: 40px 24px; | ||||||||||||||||||||||||||||||||||||||
| min-height: 100vh; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -176,6 +196,17 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article { | ||||||||||||||||||||||||||||||||||||||
| animation: fadeInUp 0.6s ease-out; | ||||||||||||||||||||||||||||||||||||||
| background: var(--surface-card-strong); | ||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border-subtle); | ||||||||||||||||||||||||||||||||||||||
| border-radius: 20px; | ||||||||||||||||||||||||||||||||||||||
| padding: 56px 56px 48px; | ||||||||||||||||||||||||||||||||||||||
| backdrop-filter: blur(14px); | ||||||||||||||||||||||||||||||||||||||
| -webkit-backdrop-filter: blur(14px); | ||||||||||||||||||||||||||||||||||||||
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| html[data-theme='light'] .article { | ||||||||||||||||||||||||||||||||||||||
| box-shadow: 0 16px 50px rgba(15, 23, 42, 0.08); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @keyframes fadeInUp { | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -228,7 +259,7 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| gap: 16px; | ||||||||||||||||||||||||||||||||||||||
| padding: 20px; | ||||||||||||||||||||||||||||||||||||||
| border-radius: 12px; | ||||||||||||||||||||||||||||||||||||||
| background: var(--surface-card-strong); | ||||||||||||||||||||||||||||||||||||||
| background: var(--surface-inset-highlight); | ||||||||||||||||||||||||||||||||||||||
| border: 1px solid var(--border-subtle); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
@@ -263,6 +294,12 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| color: var(--text-primary); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .author-links { | ||||||||||||||||||||||||||||||||||||||
| display: flex; | ||||||||||||||||||||||||||||||||||||||
| flex-wrap: wrap; | ||||||||||||||||||||||||||||||||||||||
| gap: 4px 10px; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .author-handle { | ||||||||||||||||||||||||||||||||||||||
| font-size: 0.85rem; | ||||||||||||||||||||||||||||||||||||||
| color: var(--coral-bright); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -390,6 +427,32 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| color: var(--text-secondary); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article-content :global(.evidence-figure) { | ||||||||||||||||||||||||||||||||||||||
| margin: 36px 0 40px; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article-content :global(.evidence-figure img) { | ||||||||||||||||||||||||||||||||||||||
| display: block; | ||||||||||||||||||||||||||||||||||||||
| width: 100%; | ||||||||||||||||||||||||||||||||||||||
| max-height: none; | ||||||||||||||||||||||||||||||||||||||
| height: auto; | ||||||||||||||||||||||||||||||||||||||
| margin: 0; | ||||||||||||||||||||||||||||||||||||||
| object-fit: initial; | ||||||||||||||||||||||||||||||||||||||
| border-radius: 12px; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article-content :global(.evidence-figure figcaption) { | ||||||||||||||||||||||||||||||||||||||
| margin-top: 12px; | ||||||||||||||||||||||||||||||||||||||
| color: var(--text-muted); | ||||||||||||||||||||||||||||||||||||||
| font-size: 0.9rem; | ||||||||||||||||||||||||||||||||||||||
| line-height: 1.55; | ||||||||||||||||||||||||||||||||||||||
| text-align: center; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article-content :global(.evidence-figure figcaption code) { | ||||||||||||||||||||||||||||||||||||||
| font-size: 0.86em; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /* Article Footer */ | ||||||||||||||||||||||||||||||||||||||
| .article-footer { | ||||||||||||||||||||||||||||||||||||||
| margin-top: 64px; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -536,6 +599,11 @@ const postUrl = `https://openclaw.ai/blog/${post.id}`; | |||||||||||||||||||||||||||||||||||||
| padding: 24px 16px; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article { | ||||||||||||||||||||||||||||||||||||||
| padding: 28px 22px 24px; | ||||||||||||||||||||||||||||||||||||||
| border-radius: 14px; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| .article-meta { | ||||||||||||||||||||||||||||||||||||||
| flex-direction: column; | ||||||||||||||||||||||||||||||||||||||
| align-items: flex-start; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getAuthorLinkscallsnew URL(url)when an author hasurlbut nohandle; ifauthorUrl/links.urlis a relative path or any malformed string, this throwsTypeErrorduring Astro render and can fail the blog page/build. Sincesrc/content.config.tscurrently accepts any string for these fields, a typo or internal-relative author URL is enough to trigger a hard failure instead of gracefully omitting/formatting the label.Useful? React with 👍 / 👎.