mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 19:35:28 -06:00
fix(skills): include metadata in skill size limit
This commit is contained in:
committed by
Josh Lehman
parent
887b9dd5e8
commit
a358c7abfa
@@ -277,7 +277,7 @@ result pending regardless of autonomous mode.
|
||||
| `skills.workshop.autonomous.mode` | `"auto"` | Chooses capture behavior; `auto` also enables daily collection review. |
|
||||
| `skills.workshop.approvalPolicy` | `"auto"` | Controls prompts for normal agent-initiated lifecycle calls. It never expands the isolated reviewer tool surface. |
|
||||
| `skills.workshop.maxPending` | `50` | Caps pending and quarantined proposals per workspace. |
|
||||
| `skills.workshop.maxSkillBytes` | `40000` | Caps proposal body size in bytes. |
|
||||
| `skills.workshop.maxSkillBytes` | `40000` | Caps persisted `SKILL.md` size in bytes, including retained frontmatter. |
|
||||
| `skills.workshop.allowSymlinkTargetWrites` | `false` | Allows apply through explicitly trusted workspace skill symlinks. Capture itself does not widen the trusted target list. |
|
||||
|
||||
See [Skills config](/tools/skills-config#workshop-skills-workshop) for ranges and
|
||||
|
||||
@@ -378,7 +378,7 @@ the proposal threshold, and troubleshooting.
|
||||
| `allowSymlinkTargetWrites` | `false` | Lets apply write through workspace skill symlinks whose real target is listed in `skills.load.allowSymlinkTargets`. |
|
||||
| `approvalPolicy` | `"auto"` | `"auto"` skips an additional prompt for agent-initiated `apply`, `reject`, or `quarantine` (the agent still has to call the action). `"pending"` requires approval. |
|
||||
| `maxPending` | `50` | Caps pending and quarantined proposals per workspace (1-200). |
|
||||
| `maxSkillBytes` | `40000` | Caps proposal body size in bytes (1024-200000). |
|
||||
| `maxSkillBytes` | `40000` | Caps persisted `SKILL.md` size in bytes, including retained frontmatter (1024-200000). |
|
||||
|
||||
In `propose` and `auto` modes, an isolated run of the selected model decides whether the
|
||||
completed trajectory clears the evidence-gated proposal bar. The foreground model is not prompted
|
||||
@@ -474,7 +474,7 @@ proposals remain listed with a previous-workspace marker instead of disappearing
|
||||
| Limit | Value |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| Description | 160 bytes |
|
||||
| Proposal body | `skills.workshop.maxSkillBytes` (default 40,000; hard ceiling 200,000 bytes) |
|
||||
| Persisted `SKILL.md` | `skills.workshop.maxSkillBytes` (default 40,000; hard ceiling 200,000 bytes) |
|
||||
| Support files | 64 per proposal |
|
||||
| Support file size | 256 KiB each, 2 MiB total |
|
||||
| Pending + quarantined proposals | `skills.workshop.maxPending` per workspace (default 50) |
|
||||
@@ -484,7 +484,7 @@ proposals remain listed with a previous-workspace marker instead of disappearing
|
||||
| Problem | Resolution |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Skill proposal description is too large` | Shorten `description` to 160 bytes or less. |
|
||||
| `Skill proposal content is too large` | Shorten the proposal body or raise `skills.workshop.maxSkillBytes`. |
|
||||
| `Skill proposal content is too large` | Shorten the resulting skill or raise `skills.workshop.maxSkillBytes`. |
|
||||
| `Target skill changed after proposal creation` | Revise the proposal against the current target, or create a new proposal. |
|
||||
| `Proposal scan failed` | Inspect scanner findings, then revise or quarantine the proposal. |
|
||||
| `untrusted symlink target` | Configure `skills.load.allowSymlinkTargets` and enable `skills.workshop.allowSymlinkTargetWrites` only for intentional shared skill roots. |
|
||||
|
||||
@@ -389,9 +389,9 @@ proposal-only permissions, and troubleshooting.
|
||||
</ParamField>
|
||||
|
||||
<ParamField path="skills.workshop.maxSkillBytes" type="number" default="40000">
|
||||
Maximum proposal body size in bytes (allowed range: 1024-200000). Proposal
|
||||
descriptions are hard-capped at 160 bytes separately, because they appear
|
||||
in discovery and listing output.
|
||||
Maximum persisted `SKILL.md` size in bytes, including retained frontmatter
|
||||
(allowed range: 1024-200000). Proposal descriptions are hard-capped at 160
|
||||
bytes separately, because they appear in discovery and listing output.
|
||||
</ParamField>
|
||||
|
||||
See [Skill Workshop](/tools/skill-workshop) for the proposal lifecycle, CLI
|
||||
|
||||
@@ -130,7 +130,7 @@ export function buildSkillWorkshopToolSchema(
|
||||
proposal_content: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Complete final skill body for action=create or action=update, or when action=revise changes the body. Must be the full skill content ready to become the active SKILL.md — not a plan, diff, change description, or implementation notes. On revise, omit this field to preserve the current body. On update/revise, preserve all existing content except changes the user explicitly requested. Proposal frontmatter is added automatically. Keep under configured skills.workshop.maxSkillBytes; default max is 40000 bytes.",
|
||||
"Complete final skill body for action=create or action=update, or when action=revise changes the body. Must be the full skill content ready to become the active SKILL.md — not a plan, diff, change description, or implementation notes. On revise, omit this field to preserve the current body. On update/revise, preserve all existing content except changes the user explicitly requested. Proposal frontmatter is added automatically. The persisted SKILL.md, including retained frontmatter, must fit configured skills.workshop.maxSkillBytes; default max is 40000 bytes.",
|
||||
}),
|
||||
),
|
||||
support_files: Type.Optional(
|
||||
|
||||
@@ -69,7 +69,7 @@ export type SkillsWorkshopConfig = {
|
||||
approvalPolicy?: "pending" | "auto";
|
||||
/** Maximum pending/quarantined proposals retained per workspace. */
|
||||
maxPending?: number;
|
||||
/** Maximum generated skill proposal size in bytes. */
|
||||
/** Maximum persisted SKILL.md size in bytes, including retained frontmatter. */
|
||||
maxSkillBytes?: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { stripProposalFrontmatterForSkill } from "./frontmatter.js";
|
||||
import {
|
||||
nextProposalVersion,
|
||||
prepareSkillProposalDraft,
|
||||
@@ -52,7 +53,9 @@ describe("Skill Workshop proposal draft preparation", () => {
|
||||
expect(oversized).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
message: "Skill proposal content is too large (5 bytes, max 4).",
|
||||
message: expect.stringMatching(
|
||||
/^Skill proposal content is too large \(\d+ bytes, max 4\)\.$/,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -77,6 +80,39 @@ describe("Skill Workshop proposal draft preparation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("measures the canonical persisted skill instead of the submitted body", () => {
|
||||
const maxSkillBytes = 1024;
|
||||
const input = {
|
||||
name: "boundary-skill",
|
||||
description: "Boundary skill",
|
||||
content: "x".repeat(maxSkillBytes),
|
||||
date: "2026-08-14T00:00:00.000Z",
|
||||
};
|
||||
const prepared = prepareSkillProposalDraft({
|
||||
...input,
|
||||
maxSkillBytes,
|
||||
});
|
||||
|
||||
expect(prepared).toMatchObject({
|
||||
ok: false,
|
||||
error: {
|
||||
message: expect.stringMatching(
|
||||
/^Skill proposal content is too large \(1\d{3} bytes, max 1024\)\.$/,
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
const roomy = prepareSkillProposalDraft({ ...input, maxSkillBytes: 2048 });
|
||||
if (!roomy.ok) {
|
||||
throw roomy.error.cause;
|
||||
}
|
||||
const persistedBytes = Buffer.byteLength(
|
||||
stripProposalFrontmatterForSkill(roomy.value.content),
|
||||
"utf8",
|
||||
);
|
||||
expect(prepareSkillProposalDraft({ ...input, maxSkillBytes: persistedBytes }).ok).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves version and UTF-8 description behavior", () => {
|
||||
expect(nextProposalVersion("v1")).toBe("v2");
|
||||
expect(nextProposalVersion("invalid")).toBe("v2");
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
MAX_WORKSPACE_SKILL_SUPPORT_FILE_BYTES,
|
||||
normalizeWorkspaceSkillSupportPath,
|
||||
} from "../lifecycle/workspace-skill-write.js";
|
||||
import { renderProposalMarkdown } from "./frontmatter.js";
|
||||
import { renderProposalMarkdown, stripProposalFrontmatterForSkill } from "./frontmatter.js";
|
||||
import { assertProposalContainsNoLiteralSecrets, scanProposalBundle } from "./proposal-scan.js";
|
||||
import {
|
||||
hashSkillProposalContent,
|
||||
@@ -54,7 +54,6 @@ export function prepareSkillProposalDraft(input: {
|
||||
}): Result<PreparedSkillProposalDraft, SkillProposalDraftValidationError> {
|
||||
try {
|
||||
assertProposalDescriptionWithinLimit(input.description);
|
||||
assertProposalContentWithinLimit(input.content, input.maxSkillBytes);
|
||||
const supportFiles = prepareSkillProposalSupportFiles(input.supportFiles);
|
||||
const content = renderProposalMarkdown({
|
||||
name: input.name,
|
||||
@@ -64,6 +63,10 @@ export function prepareSkillProposalDraft(input: {
|
||||
version: input.version,
|
||||
date: input.date,
|
||||
});
|
||||
assertProposalContentWithinLimit(
|
||||
stripProposalFrontmatterForSkill(content),
|
||||
input.maxSkillBytes,
|
||||
);
|
||||
const goal = normalizeOptionalString(input.goal);
|
||||
const evidence = normalizeOptionalString(input.evidence);
|
||||
const scan = scanProposalBundle(content, supportFiles, [
|
||||
|
||||
Reference in New Issue
Block a user