From 98582c07b6e601aaa485fd242bc7d14d24ccbffb Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 15 Sep 2026 06:47:51 +0000 Subject: [PATCH 1/5] fix(forms): reject agent-authored validation patterns that can hang the browser An agent asked to make "Full Name" accept at least two words wrote `^([A-Za-z]+\s?)+$`. It compiles cleanly and backtracks exponentially: 748 ms on a 26-character value, doubling with every further character. The Forms editor tab stopped responding and Chrome offered to kill the page. The same pattern is re-checked in the submit handler, so it pegs the server event loop too, and no JavaScript timeout can interrupt a match once V8 is inside it. Add analyzeRegexSource / compileUserRegex / testUserRegex to @agent-native/core/shared, which refuse patterns shaped like this rather than trying to time them out, and return a tri-state so "was not evaluated" stays distinguishable from "did not match". Forms rejects an unsafe pattern at the authoring gate shared by create-form, update-form and patch-form-fields, naming a safe rewrite so the agent can correct itself; the fill page, submit handler and public SSR runtime bound patterns already stored. The same defect was present in Calendar booking fields, whose existing input/pattern length caps are ineffective against it, and in the Slides regex-replace edit op. Also teach guard:i18n-changed-copy the export-default messagesByLocale wrapper shape, which apps using it could not otherwise satisfy. --- .changeset/bounded-user-regex.md | 26 + .../core/src/shared/bounded-regex.spec.ts | 118 +++++ packages/core/src/shared/bounded-regex.ts | 472 ++++++++++++++++++ packages/core/src/shared/index.ts | 10 + scripts/guard-i18n-changed-copy.test.ts | 30 ++ scripts/guard-i18n-changed-copy.ts | 8 +- .../app/components/booking/BookingForm.tsx | 22 +- templates/calendar/app/i18n-data.ts | 20 + templates/calendar/app/i18n/zh-TW.ts | 1 + .../calendar/server/handlers/bookings.ts | 24 +- .../lib/booking-custom-field-pattern.spec.ts | 65 +++ templates/forms/app/i18n/ar-SA.ts | 2 + templates/forms/app/i18n/de-DE.ts | 2 + templates/forms/app/i18n/en-US.ts | 2 + templates/forms/app/i18n/es-ES.ts | 2 + templates/forms/app/i18n/fr-FR.ts | 2 + templates/forms/app/i18n/hi-IN.ts | 2 + templates/forms/app/i18n/ja-JP.ts | 2 + templates/forms/app/i18n/ko-KR.ts | 2 + templates/forms/app/i18n/pt-BR.ts | 2 + templates/forms/app/i18n/zh-CN.ts | 2 + templates/forms/app/i18n/zh-TW.ts | 2 + templates/forms/app/pages/FormFillPage.tsx | 12 +- templates/forms/server/lib/public-form-ssr.ts | 31 +- .../forms/server/lib/submission-validation.ts | 17 +- templates/forms/server/lib/validate-fields.ts | 21 +- .../lib/validation-pattern-redos.spec.ts | 116 +++++ templates/forms/shared/field-schema.ts | 4 +- .../slides/server/lib/slide-content-patch.ts | 10 + 29 files changed, 992 insertions(+), 37 deletions(-) create mode 100644 .changeset/bounded-user-regex.md create mode 100644 packages/core/src/shared/bounded-regex.spec.ts create mode 100644 packages/core/src/shared/bounded-regex.ts create mode 100644 templates/calendar/server/lib/booking-custom-field-pattern.spec.ts create mode 100644 templates/forms/server/lib/validation-pattern-redos.spec.ts diff --git a/.changeset/bounded-user-regex.md b/.changeset/bounded-user-regex.md new file mode 100644 index 00000000000..c5842382fcb --- /dev/null +++ b/.changeset/bounded-user-regex.md @@ -0,0 +1,26 @@ +--- +"@agent-native/core": patch +--- + +Add `compileUserRegex`, `testUserRegex`, and `analyzeRegexSource` to +`@agent-native/core/shared` for evaluating regular expressions that come from an +agent or an end user rather than from source. + +`new RegExp(source).test(value)` is not a bounded operation, and JavaScript has +no way to time a match out once V8 is inside it. A pattern an LLM routinely +writes to mean "at least two words" — `^([A-Za-z]+\s?)+$` — is 17 characters, +compiles cleanly, and backtracks exponentially: a 26-character non-matching +value already costs ~750 ms and the cost doubles with every further character. +Stored on a form field it froze the respondent's tab and, because the same +pattern was re-checked on submit, the request handler's event loop with it. +Capping the input length does not help, because the blowup is reached well +inside any sane cap. + +`analyzeRegexSource` recognises the ambiguity signatures that cause +super-linear backtracking (nested and adjacent overlapping repetition, nullable +parts under an unbounded repeat, overlapping single-atom alternatives) and +refuses those patterns instead of running them. Patterns it clears are still +evaluated against a capped input. `testUserRegex` returns a tri-state result so +"did not match" and "was not evaluated" stay distinguishable — collapsing the +second into the first is how an unenforceable rule silently becomes an +enforced-looking one. diff --git a/packages/core/src/shared/bounded-regex.spec.ts b/packages/core/src/shared/bounded-regex.spec.ts new file mode 100644 index 00000000000..51bbe9b0a1e --- /dev/null +++ b/packages/core/src/shared/bounded-regex.spec.ts @@ -0,0 +1,118 @@ +import { describe, it, expect } from "vitest"; + +import { + MAX_USER_REGEX_INPUT_LENGTH, + MAX_USER_REGEX_LENGTH, + analyzeRegexSource, + compileUserRegex, + testUserRegex, +} from "./bounded-regex.js"; + +/** + * The reported hang came from an agent writing a validation rule for "Full Name + * must be at least two words". These are the patterns an LLM actually produces + * for that request; the exponential ones are the bug. + */ +const CATASTROPHIC = [ + "^([A-Za-z]+\\s?)+$", + "^([A-Za-z]+(\\s|-|')?)+[A-Za-z]+$", + "^(a+)+$", + "^(\\w+)+$", + "^([a-z]*)*$", + "^(a|a)*$", + "^(\\d|\\w)+$", + "^(\\s*\\S+)*$", +]; + +/** Patterns that must keep working — including correct "two words" rules. */ +const LINEAR = [ + "^\\s*\\S+(\\s+\\S+)+\\s*$", + "^\\w+(\\s+\\w+)+$", + "^(\\w+\\s+)+\\w+$", + "^([a-zA-Z]+ )+[a-zA-Z]+$", + "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "^\\d{3}-\\d{3}-\\d{4}$", + "^(\\+\\d{1,3}\\s?)?\\(?\\d{3}\\)?[\\s.-]?\\d{3}[\\s.-]?\\d{4}$", + "^[A-Z]{2}\\d{4}$", + "^(cat|car)+$", + "^https?://\\S+$", + "^.{8,64}$", + "^(?:Mr|Mrs|Ms|Dr)\\.? [A-Za-z]+$", +]; + +/** Long enough that an exponential pattern would not return this decade. */ +const HOSTILE_INPUT = + "Jonathan Alexander Montgomery Wellington Fitzgerald Smith Junior Esquire!"; + +describe("analyzeRegexSource", () => { + it.each(CATASTROPHIC)("rejects the super-linear pattern %s", (source) => { + const verdict = analyzeRegexSource(source); + expect(verdict.safe).toBe(false); + if (!verdict.safe) expect(verdict.reason).toBeTruthy(); + }); + + it.each(LINEAR)("accepts the linear pattern %s", (source) => { + expect(analyzeRegexSource(source)).toEqual({ safe: true }); + }); + + it("refuses to clear a pattern it cannot parse", () => { + expect(analyzeRegexSource("^(unclosed").safe).toBe(false); + }); +}); + +describe("compileUserRegex", () => { + it("returns a usable regex for a safe pattern", () => { + const result = compileUserRegex("^\\w+(\\s+\\w+)+$"); + expect(result.status).toBe("ok"); + if (result.status === "ok") { + expect(result.regex.test("Ada Lovelace")).toBe(true); + expect(result.regex.test("Ada")).toBe(false); + } + }); + + it("separates invalid syntax from an unsafe shape", () => { + expect(compileUserRegex("^[a-").status).toBe("invalid-syntax"); + expect(compileUserRegex("^([A-Za-z]+\\s?)+$").status).toBe("unsafe"); + }); + + it("rejects an over-long pattern before compiling it", () => { + const result = compileUserRegex("a".repeat(MAX_USER_REGEX_LENGTH + 1)); + expect(result.status).toBe("too-long"); + }); +}); + +describe("testUserRegex", () => { + it("evaluates a safe pattern normally", () => { + expect(testUserRegex("^\\w+(\\s+\\w+)+$", "Ada Lovelace")).toEqual({ + status: "match", + }); + expect(testUserRegex("^\\w+(\\s+\\w+)+$", "Ada")).toEqual({ + status: "no-match", + }); + }); + + it("reports an unsafe pattern as unevaluated, never as no-match", () => { + const result = testUserRegex("^([A-Za-z]+\\s?)+$", HOSTILE_INPUT); + expect(result.status).toBe("unevaluated"); + // The distinction is the whole point: a caller must not be able to read + // "we refused to run this" as "the value failed the rule". + expect(result.status).not.toBe("no-match"); + }); + + it("reports an over-long value as unevaluated", () => { + const result = testUserRegex( + "^\\w+$", + "a".repeat(MAX_USER_REGEX_INPUT_LENGTH + 1), + ); + expect(result.status).toBe("unevaluated"); + }); + + it("returns within a bounded time for the pattern that froze the tab", () => { + const started = Date.now(); + for (const source of CATASTROPHIC) { + expect(testUserRegex(source, HOSTILE_INPUT).status).toBe("unevaluated"); + } + // Unguarded, the first pattern alone does not finish in this millennium. + expect(Date.now() - started).toBeLessThan(1000); + }); +}); diff --git a/packages/core/src/shared/bounded-regex.ts b/packages/core/src/shared/bounded-regex.ts new file mode 100644 index 00000000000..c092af5d004 --- /dev/null +++ b/packages/core/src/shared/bounded-regex.ts @@ -0,0 +1,472 @@ +/** + * Bounded evaluation of regular expressions that come from an agent or an end + * user rather than from source. + * + * `new RegExp(source).test(value)` is not a bounded operation. A pattern an LLM + * routinely writes for "must be at least two words" — `^([A-Za-z]+\s?)+$` — is + * 17 characters, compiles cleanly, and backtracks exponentially: matching it + * against a 26-character non-matching value already costs ~750 ms and doubles + * with every additional character. Stored on a form field it freezes the + * respondent's tab and, because the same pattern is re-checked server side on + * submit, the request handler's event loop with it. Capping the input length + * does not help; the blowup is reached well inside any sane cap. + * + * There is no timeout on `RegExp` in JavaScript, so the bound has to come from + * refusing to run patterns whose shape permits the blowup. `analyzeRegexSource` + * is that refusal, and it is deliberately a heuristic: it recognises the + * ambiguity signatures that cause super-linear backtracking rather than proving + * their absence. Patterns it clears are still evaluated against a capped input. + * + * Callers get a tri-state result. "Did not match" and "was not evaluated" are + * different answers, and collapsing the second into the first is how an + * unenforceable rule silently becomes an enforced-looking one. + */ + +/** Longest pattern source accepted. Real validation patterns are far shorter. */ +export const MAX_USER_REGEX_LENGTH = 512; + +/** Longest value fed to a user-authored pattern. */ +export const MAX_USER_REGEX_INPUT_LENGTH = 4096; + +/** Nesting depth beyond which the analyzer stops trusting its own reading. */ +const MAX_GROUP_DEPTH = 12; + +/** + * Representative characters used to approximate "can these two atoms match the + * same character". A fixed probe alphabet keeps the comparison cheap and lets + * the engine itself answer the question for each single character. + */ +const PROBE_CHARS = [ + "a", + "Z", + "5", + "0", + " ", + "\t", + "\n", + "_", + "-", + "'", + ".", + "@", + "/", + "#", + "!", +]; + +type AtomKind = + | "group" + | "class" + | "literal" + | "escape" + | "dot" + | "anchor" + | "backref"; + +interface RegexAtom { + kind: AtomKind; + /** Atom source with its quantifier stripped. */ + source: string; + /** Present for groups: the alternation branches of the group body. */ + branches?: RegexAtom[][]; + min: number; + /** `Number.POSITIVE_INFINITY` for `*`, `+` and `{n,}`. */ + max: number; +} + +export type RegexSafetyVerdict = + | { safe: true } + | { safe: false; reason: string }; + +interface ParseState { + source: string; + index: number; + depth: number; + bailed: boolean; +} + +function parseQuantifier(state: ParseState): { min: number; max: number } { + const { source } = state; + const ch = source[state.index]; + let min = 1; + let max = 1; + if (ch === "*") { + state.index += 1; + min = 0; + max = Number.POSITIVE_INFINITY; + } else if (ch === "+") { + state.index += 1; + min = 1; + max = Number.POSITIVE_INFINITY; + } else if (ch === "?") { + state.index += 1; + min = 0; + max = 1; + } else if (ch === "{") { + const close = source.indexOf("}", state.index); + const body = close === -1 ? "" : source.slice(state.index + 1, close); + const match = /^(\d+)(,(\d*)?)?$/.exec(body); + if (close !== -1 && match) { + state.index = close + 1; + min = Number(match[1]); + max = match[2] + ? match[3] + ? Number(match[3]) + : Number.POSITIVE_INFINITY + : min; + } + } + // A lazy or possessive marker changes match semantics, not the ambiguity + // that drives backtracking cost. + if (source[state.index] === "?" || source[state.index] === "+") { + if (max !== 1 || min !== 1) state.index += 1; + } + return { min, max }; +} + +function parseCharClass(state: ParseState): string { + const start = state.index; + state.index += 1; // consume "[" + if (state.source[state.index] === "^") state.index += 1; + if (state.source[state.index] === "]") state.index += 1; + while (state.index < state.source.length) { + const ch = state.source[state.index]; + if (ch === "\\") { + state.index += 2; + continue; + } + if (ch === "]") { + state.index += 1; + return state.source.slice(start, state.index); + } + state.index += 1; + } + state.bailed = true; + return state.source.slice(start); +} + +function parseSequence(state: ParseState): RegexAtom[] { + const atoms: RegexAtom[] = []; + while (state.index < state.source.length && !state.bailed) { + const ch = state.source[state.index]; + if (ch === "|" || ch === ")") break; + + let kind: AtomKind = "literal"; + let source = ""; + let branches: RegexAtom[][] | undefined; + + if (ch === "(") { + if (state.depth >= MAX_GROUP_DEPTH) { + state.bailed = true; + break; + } + const start = state.index; + state.index += 1; + let capturing = true; + if (state.source[state.index] === "?") { + const marker = state.source.slice(state.index, state.index + 3); + if ( + marker.startsWith("?:") || + marker.startsWith("?=") || + marker.startsWith("?!") + ) { + state.index += 2; + capturing = false; + } else if (marker === "?<=" || marker === "?", state.index); + if (close === -1) { + state.bailed = true; + break; + } + state.index = close + 1; + } + } + state.depth += 1; + branches = parseAlternation(state); + state.depth -= 1; + if (state.source[state.index] !== ")") { + state.bailed = true; + break; + } + state.index += 1; + kind = "group"; + source = state.source.slice(start, state.index); + void capturing; + } else if (ch === "[") { + source = parseCharClass(state); + kind = "class"; + } else if (ch === "\\") { + source = state.source.slice(state.index, state.index + 2); + state.index += 2; + kind = /^\\\d$/.test(source) ? "backref" : "escape"; + } else if (ch === ".") { + state.index += 1; + source = "."; + kind = "dot"; + } else if (ch === "^" || ch === "$") { + state.index += 1; + source = ch; + kind = "anchor"; + } else { + state.index += 1; + source = ch; + kind = "literal"; + } + + const { min, max } = parseQuantifier(state); + atoms.push({ kind, source, branches, min, max }); + } + return atoms; +} + +function parseAlternation(state: ParseState): RegexAtom[][] { + const branches: RegexAtom[][] = [parseSequence(state)]; + while (state.source[state.index] === "|" && !state.bailed) { + state.index += 1; + branches.push(parseSequence(state)); + } + return branches; +} + +function isNullable(atom: RegexAtom): boolean { + if (atom.kind === "anchor") return true; + if (atom.min === 0) return true; + if (atom.kind === "group" && atom.branches) { + return atom.branches.some((branch) => branch.every(isNullable)); + } + return false; +} + +function isUnbounded(atom: RegexAtom): boolean { + return atom.max === Number.POSITIVE_INFINITY; +} + +/** + * Characters this atom can match in a single position. Derived by asking the + * engine itself, one probe character at a time, so a single-character match can + * never be the expensive case. + */ +function charSetOf(atom: RegexAtom): Set { + if (atom.kind === "anchor" || atom.kind === "backref") return new Set(); + if (atom.kind === "group") { + const set = new Set(); + for (const branch of atom.branches ?? []) { + for (const ch of leadingCharSet(branch)) set.add(ch); + } + return set; + } + const set = new Set(); + let probe: RegExp; + try { + probe = new RegExp(`^(?:${atom.source})$`); + } catch { + return set; + } + for (const ch of PROBE_CHARS) { + if (probe.test(ch)) set.add(ch); + } + return set; +} + +/** Characters a branch can start with, looking past nullable leading atoms. */ +function leadingCharSet(branch: RegexAtom[]): Set { + const set = new Set(); + for (const atom of branch) { + for (const ch of charSetOf(atom)) set.add(ch); + if (!isNullable(atom)) break; + } + return set; +} + +function overlaps(a: Set, b: Set): boolean { + for (const ch of a) if (b.has(ch)) return true; + return false; +} + +/** Atoms that actually consume input — anchors carry no matching cost. */ +function consuming(branch: RegexAtom[]): RegexAtom[] { + return branch.filter((atom) => atom.kind !== "anchor"); +} + +function describe(atom: RegexAtom): string { + const quantifier = + atom.max === Number.POSITIVE_INFINITY + ? atom.min === 0 + ? "*" + : atom.min === 1 + ? "+" + : `{${atom.min},}` + : atom.min === 0 && atom.max === 1 + ? "?" + : ""; + return `${atom.source}${quantifier}`; +} + +function analyzeRepeatedGroup(atom: RegexAtom): string | null { + const branches = atom.branches ?? []; + + for (const branch of branches) { + const atoms = consuming(branch); + if (atoms.length === 0) continue; + + // An inner repetition that may match nothing lets the outer repetition + // split the same input in exponentially many ways. + if (atoms.length > 1) { + const nullable = atoms.find(isNullable); + const unbounded = atoms.find( + (candidate) => isUnbounded(candidate) && candidate !== nullable, + ); + if (nullable && unbounded) { + return `repeated group \`${describe(atom)}\` contains both an optional part (\`${describe(nullable)}\`) and an unbounded repetition (\`${describe(unbounded)}\`), so the same text can be split in exponentially many ways`; + } + } + + // Neighbouring repetitions competing for the same characters. + for (let i = 0; i + 1 < atoms.length; i += 1) { + const left = atoms[i]; + const right = atoms[i + 1]; + if (!isUnbounded(left) && !isUnbounded(right)) continue; + if (!isNullable(left) && !isNullable(right) && !isUnbounded(left)) + continue; + if (overlaps(charSetOf(left), charSetOf(right))) { + return `repeated group \`${describe(atom)}\` has adjacent repetitions (\`${describe(left)}\` and \`${describe(right)}\`) that match the same characters`; + } + } + + // The junction between two iterations of the outer repetition. + const first = atoms[0]; + const last = atoms[atoms.length - 1]; + if ( + (isUnbounded(first) || isUnbounded(last)) && + overlaps(charSetOf(last), charSetOf(first)) + ) { + return `repeated group \`${describe(atom)}\` can match the same characters at the start and end of each repetition`; + } + + if (atoms.every(isNullable)) { + return `repeated group \`${describe(atom)}\` can match an empty string`; + } + } + + // Alternatives inside a repetition that accept the same single-atom input. + for (let i = 0; i < branches.length; i += 1) { + for (let j = i + 1; j < branches.length; j += 1) { + const a = consuming(branches[i]); + const b = consuming(branches[j]); + if (a.length !== 1 || b.length !== 1) continue; + if (overlaps(charSetOf(a[0]), charSetOf(b[0]))) { + return `repeated group \`${describe(atom)}\` has alternatives (\`${describe(a[0])}\` and \`${describe(b[0])}\`) that match the same characters`; + } + } + } + + return null; +} + +function walk(branches: RegexAtom[][]): string | null { + for (const branch of branches) { + for (const atom of branch) { + if (atom.kind !== "group") continue; + if (isUnbounded(atom)) { + const reason = analyzeRepeatedGroup(atom); + if (reason) return reason; + } + const nested = walk(atom.branches ?? []); + if (nested) return nested; + } + } + return null; +} + +/** + * Report whether `source` is shaped like a pattern that can backtrack + * super-linearly. A `safe: true` verdict means no known blowup signature was + * found, not that the pattern is provably linear. + */ +export function analyzeRegexSource(source: string): RegexSafetyVerdict { + const state: ParseState = { source, index: 0, depth: 0, bailed: false }; + const branches = parseAlternation(state); + if (state.bailed || state.index < source.length) { + return { + safe: false, + reason: + "pattern uses constructs this validator cannot analyze for catastrophic backtracking", + }; + } + const reason = walk(branches); + return reason ? { safe: false, reason } : { safe: true }; +} + +export type UserRegexCompileResult = + | { status: "ok"; regex: RegExp } + | { status: "too-long"; message: string } + | { status: "invalid-syntax"; message: string } + | { status: "unsafe"; message: string }; + +/** + * Compile a pattern that came from outside source control, rejecting sources + * that are over-long, syntactically invalid, or shaped like a ReDoS. + */ +export function compileUserRegex( + source: string, + options: { flags?: string } = {}, +): UserRegexCompileResult { + if (source.length > MAX_USER_REGEX_LENGTH) { + return { + status: "too-long", + message: `pattern is ${source.length} characters; the limit is ${MAX_USER_REGEX_LENGTH}`, + }; + } + + let regex: RegExp; + try { + regex = new RegExp(source, options.flags); + } catch (error) { + return { + status: "invalid-syntax", + message: error instanceof Error ? error.message : String(error), + }; + } + + const verdict = analyzeRegexSource(source); + if (!verdict.safe) { + return { status: "unsafe", message: verdict.reason }; + } + + return { status: "ok", regex }; +} + +export type UserRegexTestResult = + | { status: "match" } + | { status: "no-match" } + | { status: "unevaluated"; reason: string }; + +/** + * Test `value` against a user-authored pattern within a bounded budget. + * + * `unevaluated` is a distinct outcome on purpose: the caller has to decide what + * an unenforceable rule means for its surface, and cannot accidentally read it + * as "the value passed". + */ +export function testUserRegex( + source: string, + value: string, + options: { flags?: string } = {}, +): UserRegexTestResult { + const compiled = compileUserRegex(source, options); + if (compiled.status !== "ok") { + return { status: "unevaluated", reason: compiled.message }; + } + if (value.length > MAX_USER_REGEX_INPUT_LENGTH) { + return { + status: "unevaluated", + reason: `value is ${value.length} characters; the limit for pattern checks is ${MAX_USER_REGEX_INPUT_LENGTH}`, + }; + } + return compiled.regex.test(value) + ? { status: "match" } + : { status: "no-match" }; +} diff --git a/packages/core/src/shared/index.ts b/packages/core/src/shared/index.ts index a7bb6312b88..d8d05bea496 100644 --- a/packages/core/src/shared/index.ts +++ b/packages/core/src/shared/index.ts @@ -30,6 +30,16 @@ export { type SignInJourneyInput, } from "./sign-in-journey.js"; export { truncate } from "./truncate.js"; +export { + MAX_USER_REGEX_INPUT_LENGTH, + MAX_USER_REGEX_LENGTH, + analyzeRegexSource, + compileUserRegex, + testUserRegex, + type RegexSafetyVerdict, + type UserRegexCompileResult, + type UserRegexTestResult, +} from "./bounded-regex.js"; export { isHumanReadableDocumentTitle, normalizeDocumentTitle, diff --git a/scripts/guard-i18n-changed-copy.test.ts b/scripts/guard-i18n-changed-copy.test.ts index de9188a63d2..20cbd3eb2c6 100644 --- a/scripts/guard-i18n-changed-copy.test.ts +++ b/scripts/guard-i18n-changed-copy.test.ts @@ -58,6 +58,36 @@ describe("changed copy localization coverage", () => { ); }); + it("accepts a wrapper that re-exports the locale instead of spreading it", () => { + // templates/calendar and templates/brain use this shape. It forwards the + // inline block just as directly as the spread form. + const source = "/catalog/i18n-data.ts"; + assert.equal( + hasForwardedInlineLocaleUpdate( + "es-ES", + new Set(["es-ES"]), + source, + source, + `import { messagesByLocale } from "../i18n-data";\n\nexport default messagesByLocale["es-ES"];\n`, + ), + true, + ); + }); + + it("still fails a re-export wrapper when that locale did not change", () => { + const source = "/catalog/i18n-data.ts"; + assert.equal( + hasForwardedInlineLocaleUpdate( + "es-ES", + new Set(["fr-FR"]), + source, + source, + `export default messagesByLocale["es-ES"];\n`, + ), + false, + ); + }); + it("still fails when the target locale has no inline update", () => { const source = "/catalog/i18n-data.ts"; assert.equal( diff --git a/scripts/guard-i18n-changed-copy.ts b/scripts/guard-i18n-changed-copy.ts index ce508fa52c7..187bff9d2fd 100644 --- a/scripts/guard-i18n-changed-copy.ts +++ b/scripts/guard-i18n-changed-copy.ts @@ -65,8 +65,14 @@ export function hasForwardedInlineLocaleUpdate( return false; } + // Two wrapper shapes forward the same inline locale block: spreading it into + // a local object (`...messagesByLocale["es-ES"]`) and re-exporting it + // directly (`export default messagesByLocale["es-ES"]`). Recognising only the + // first left every app using the second unable to satisfy this guard except + // through an i18n-copy-ignore marker, which is the escape hatch this check + // exists to make unnecessary. return new RegExp( - `^\\s*\\.\\.\\.\\s*messagesByLocale\\s*\\[\\s*["']${locale}["']\\s*\\]`, + `(?:\\.\\.\\.|\\bexport\\s+default)\\s*messagesByLocale\\s*\\[\\s*["']${locale}["']\\s*\\]`, "m", ).test(wrapperText); } diff --git a/templates/calendar/app/components/booking/BookingForm.tsx b/templates/calendar/app/components/booking/BookingForm.tsx index c83ce1e6293..a52bc382b85 100644 --- a/templates/calendar/app/components/booking/BookingForm.tsx +++ b/templates/calendar/app/components/booking/BookingForm.tsx @@ -1,5 +1,6 @@ import { useT } from "@agent-native/core/client/i18n"; import { Turnstile } from "@agent-native/core/client/ui"; +import { testUserRegex } from "@agent-native/core/shared"; import type { CustomField } from "@shared/api"; import { IconX } from "@tabler/icons-react"; import { useState } from "react"; @@ -108,14 +109,19 @@ export function BookingForm({ } } if (field.pattern && typeof value === "string" && value) { - try { - const re = new RegExp(field.pattern); - if (!re.test(value)) { - errors[field.id] = - field.patternError || - t("bookingLinks.fieldFormatError", { label: field.label }); - } - } catch {} + // An unrunnable pattern is not a passing one. Swallowing it here used + // to mean a broken rule silently validated everything, while a + // catastrophically backtracking one froze the booker tab outright. + const result = testUserRegex(field.pattern, value); + if (result.status === "unevaluated") { + errors[field.id] = t("bookingLinks.fieldPatternUncheckable", { + label: field.label, + }); + } else if (result.status === "no-match") { + errors[field.id] = + field.patternError || + t("bookingLinks.fieldFormatError", { label: field.label }); + } } } setFieldErrors(errors); diff --git a/templates/calendar/app/i18n-data.ts b/templates/calendar/app/i18n-data.ts index 0a5e56fc533..c4f3e31ee4e 100644 --- a/templates/calendar/app/i18n-data.ts +++ b/templates/calendar/app/i18n-data.ts @@ -529,6 +529,8 @@ const enUS = { hoursShort: "{{count}} hr", invalidEmail: "Invalid email: {{email}}", fieldFormatError: "{{label}} does not match the expected format", + fieldPatternUncheckable: + "The rule for {{label}} can't be checked. Ask the organizer to fix it.", fieldRequired: "{{label}} is required", linkDisabled: "{{title}} disabled", linkEnabled: "{{title}} enabled", @@ -7851,6 +7853,8 @@ const translatedCalendarRawBurnDown = { cancelOrReschedule: "取消或重新安排", confirmationSent: "你已完成!确认邮件已发送到你的邮箱。", fieldFormatError: "{{label}} 与预期格式不匹配", + fieldPatternUncheckable: + "{{label}} 的校验规则无法检查,请联系组织者修复。", fieldRequired: "{{label}} 为必填项", meetingLink: "会议链接", needToMakeChanges: "需要更改吗?", @@ -8024,6 +8028,8 @@ const translatedCalendarRawBurnDown = { confirmationSent: "Todo listo. Se ha enviado una confirmación a tu correo.", fieldFormatError: "{{label}} no coincide con el formato esperado", + fieldPatternUncheckable: + "La regla de {{label}} no se puede comprobar. Pide al organizador que la corrija.", fieldRequired: "{{label}} es obligatorio", meetingLink: "Enlace de la reunión", needToMakeChanges: "¿Necesitas hacer cambios?", @@ -8207,6 +8213,8 @@ const translatedCalendarRawBurnDown = { confirmationSent: "Tout est prêt ! Une confirmation a été envoyée à votre adresse e-mail.", fieldFormatError: "{{label}} ne correspond pas au format attendu", + fieldPatternUncheckable: + "La règle de {{label}} ne peut pas être vérifiée. Demandez à l'organisateur de la corriger.", fieldRequired: "{{label}} est obligatoire", meetingLink: "Lien de réunion", needToMakeChanges: "Besoin de modifier ?", @@ -8392,6 +8400,8 @@ const translatedCalendarRawBurnDown = { confirmationSent: "Alles erledigt! Eine Bestätigung wurde an deine E-Mail gesendet.", fieldFormatError: "{{label}} entspricht nicht dem erwarteten Format", + fieldPatternUncheckable: + "Die Regel für {{label}} kann nicht geprüft werden. Bitten Sie den Organisator, sie zu korrigieren.", fieldRequired: "{{label}} ist erforderlich", meetingLink: "Meeting-Link", needToMakeChanges: "Möchtest du etwas ändern?", @@ -8578,6 +8588,8 @@ const translatedCalendarRawBurnDown = { cancelOrReschedule: "キャンセルまたは変更", confirmationSent: "完了しました。確認メールを送信しました。", fieldFormatError: "{{label}} が想定形式と一致しません", + fieldPatternUncheckable: + "{{label}} のルールは検証できません。主催者に修正を依頼してください。", fieldRequired: "{{label}} は必須です", meetingLink: "ミーティングリンク", needToMakeChanges: "変更が必要ですか?", @@ -8756,6 +8768,8 @@ const translatedCalendarRawBurnDown = { cancelOrReschedule: "취소 또는 일정 변경", confirmationSent: "완료되었습니다. 확인 이메일을 보냈습니다.", fieldFormatError: "{{label}}이(가) 예상 형식과 일치하지 않습니다", + fieldPatternUncheckable: + "{{label}} 규칙을 확인할 수 없습니다. 주최자에게 수정을 요청하세요.", fieldRequired: "{{label}}은(는) 필수입니다", meetingLink: "회의 링크", needToMakeChanges: "변경이 필요하신가요?", @@ -8935,6 +8949,8 @@ const translatedCalendarRawBurnDown = { confirmationSent: "Tudo certo! Uma confirmação foi enviada para o seu e-mail.", fieldFormatError: "{{label}} não corresponde ao formato esperado", + fieldPatternUncheckable: + "A regra de {{label}} não pode ser verificada. Peça ao organizador para corrigi-la.", fieldRequired: "{{label}} é obrigatório", meetingLink: "Link da reunião", needToMakeChanges: "Precisa fazer alterações?", @@ -9118,6 +9134,8 @@ const translatedCalendarRawBurnDown = { cancelOrReschedule: "रद्द करें या पुनर्निर्धारित करें", confirmationSent: "सब तैयार है! पुष्टि आपके ईमेल पर भेज दी गई है।", fieldFormatError: "{{label}} अपेक्षित फ़ॉर्मेट से मेल नहीं खाता", + fieldPatternUncheckable: + "{{label}} का नियम जाँचा नहीं जा सकता। कृपया आयोजक से इसे ठीक करने को कहें।", fieldRequired: "{{label}} आवश्यक है", meetingLink: "मीटिंग लिंक", needToMakeChanges: "बदलाव करने हैं?", @@ -9294,6 +9312,8 @@ const translatedCalendarRawBurnDown = { cancelOrReschedule: "إلغاء أو إعادة جدولة", confirmationSent: "كل شيء جاهز! تم إرسال تأكيد إلى بريدك الإلكتروني.", fieldFormatError: "{{label}} لا يطابق التنسيق المتوقع", + fieldPatternUncheckable: + "تعذّر التحقق من قاعدة {{label}}. يرجى الطلب من المنظّم إصلاحها.", fieldRequired: "{{label}} مطلوب", meetingLink: "رابط الاجتماع", needToMakeChanges: "هل تحتاج إلى إجراء تغييرات؟", diff --git a/templates/calendar/app/i18n/zh-TW.ts b/templates/calendar/app/i18n/zh-TW.ts index e6feae6cd0a..c1ebaf70274 100644 --- a/templates/calendar/app/i18n/zh-TW.ts +++ b/templates/calendar/app/i18n/zh-TW.ts @@ -500,6 +500,7 @@ const messages = { hoursShort: "{{count}}小時", invalidEmail: "無效電子郵件:{{email}}", fieldFormatError: "{{label}} 與預期格式不匹配", + fieldPatternUncheckable: "{{label}} 的檢核規則無法檢查,請聯絡主辦人修正。", fieldRequired: "{{label}} 為必填項", linkDisabled: "{{title}} 停用", linkEnabled: "{{title}}已啟用", diff --git a/templates/calendar/server/handlers/bookings.ts b/templates/calendar/server/handlers/bookings.ts index acd06af4957..c20fceeb3a4 100644 --- a/templates/calendar/server/handlers/bookings.ts +++ b/templates/calendar/server/handlers/bookings.ts @@ -9,6 +9,7 @@ import { verifyCaptcha, } from "@agent-native/core/server"; import { getSetting, getUserSetting } from "@agent-native/core/settings"; +import { testUserRegex } from "@agent-native/core/shared"; import { accessFilter } from "@agent-native/core/sharing"; import { track } from "@agent-native/core/tracking"; import { and, eq, gt, gte, inArray, lt, lte, ne, or, sql } from "drizzle-orm"; @@ -1342,21 +1343,18 @@ export const createBooking = defineEventHandler(async (event: H3Event) => { return { error: `${field.label} must be a valid email address` }; } if (field.pattern && typeof value === "string" && value) { - // Cap input length to mitigate ReDoS on user-defined patterns - const safeValue = value.slice(0, 1000); - let re: RegExp; - // Limit pattern length and reject obviously dangerous constructs - if (field.pattern.length > 200) { + // Capping the input length does not bound a catastrophically + // backtracking pattern: `^([A-Za-z]+\s?)+$` already runs for hours on a + // 58-character value, well inside any cap. The bound has to come from + // refusing to evaluate patterns shaped like that at all. + const result = testUserRegex(field.pattern, value); + if (result.status === "unevaluated") { setResponseStatus(event, 400); - return { error: `Validation pattern too long for ${field.label}` }; - } - try { - re = new RegExp(field.pattern); - } catch { - setResponseStatus(event, 400); - return { error: `Invalid validation pattern for ${field.label}` }; + return { + error: `Invalid validation pattern for ${field.label}: ${result.reason}`, + }; } - if (!re.test(safeValue)) { + if (result.status === "no-match") { setResponseStatus(event, 400); return { error: diff --git a/templates/calendar/server/lib/booking-custom-field-pattern.spec.ts b/templates/calendar/server/lib/booking-custom-field-pattern.spec.ts new file mode 100644 index 00000000000..2e60f3d5a07 --- /dev/null +++ b/templates/calendar/server/lib/booking-custom-field-pattern.spec.ts @@ -0,0 +1,65 @@ +/** + * Booking links let the organizer put a regex on a custom field, and both the + * booker's browser and the booking handler run it. The same shape that froze + * the Forms editor tab reaches both here. + * + * The handler previously capped the value at 1000 characters and the pattern at + * 200, which reads like a ReDoS mitigation but is not one: the pattern below is + * 17 characters and blows up at roughly 40 characters of input, far inside both + * caps. These assertions are time-bounded so a regression hangs the test rather + * than passing quietly. + */ +import { testUserRegex } from "@agent-native/core/shared"; +import { describe, expect, it } from "vitest"; + +const CATASTROPHIC_PATTERN = "^([A-Za-z]+\\s?)+$"; +const SAFE_PATTERN = "^\\S+(\\s+\\S+)+$"; +const HOSTILE_VALUE = "Jonathan Alexander Montgomery Wellington Smith Junior!"; + +function withinBudget(budgetMs: number, body: () => T): T { + const started = Date.now(); + const result = body(); + expect(Date.now() - started).toBeLessThan(budgetMs); + return result; +} + +describe("booking custom field patterns", () => { + it("refuses a catastrophic pattern instead of running it", () => { + const result = withinBudget(1000, () => + testUserRegex(CATASTROPHIC_PATTERN, HOSTILE_VALUE), + ); + expect(result.status).toBe("unevaluated"); + }); + + it("does not become slower as the value grows", () => { + // The pre-fix cost doubled with every added character. Anything still + // exponential cannot clear this budget at 400 characters. + withinBudget(1000, () => { + testUserRegex(CATASTROPHIC_PATTERN, "a".repeat(400) + "!"); + }); + }); + + it("reports an uncheckable rule distinctly from a failing one", () => { + // A booking must not be accepted because the rule could not be evaluated, + // and must not be rejected with a "wrong format" message that blames the + // booker for the organizer's pattern. + expect(testUserRegex(CATASTROPHIC_PATTERN, "Ada Lovelace").status).toBe( + "unevaluated", + ); + expect(testUserRegex(SAFE_PATTERN, "Ada").status).toBe("no-match"); + }); + + it("keeps ordinary organizer patterns working", () => { + expect(testUserRegex(SAFE_PATTERN, "Ada Lovelace").status).toBe("match"); + expect(testUserRegex("^\\d{3}-\\d{4}$", "555-0100").status).toBe("match"); + expect(testUserRegex("^[A-Z]{2}\\d{4}$", "AB1234").status).toBe("match"); + expect( + testUserRegex("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$", "a@b.co") + .status, + ).toBe("match"); + }); + + it("reports an invalid pattern as uncheckable rather than swallowing it", () => { + expect(testUserRegex("^[a-", "anything").status).toBe("unevaluated"); + }); +}); diff --git a/templates/forms/app/i18n/ar-SA.ts b/templates/forms/app/i18n/ar-SA.ts index daf7fe2379e..bcc8c2a7388 100644 --- a/templates/forms/app/i18n/ar-SA.ts +++ b/templates/forms/app/i18n/ar-SA.ts @@ -353,6 +353,8 @@ const messages = { responseSubmitted: "تم إرسال الرد", noFields: "لا يحتوي هذا النموذج على حقول بعد.", failedSubmit: "فشل إرسال النموذج", + uncheckablePattern: + "تعذّر التحقق من قاعدة هذا النموذج الخاصة بـ {label}. يرجى الطلب من مالك النموذج إصلاحها.", }, responseInsights: { unavailable: "الرؤى غير متاحة", diff --git a/templates/forms/app/i18n/de-DE.ts b/templates/forms/app/i18n/de-DE.ts index eacf7537d28..9098ad9faaa 100644 --- a/templates/forms/app/i18n/de-DE.ts +++ b/templates/forms/app/i18n/de-DE.ts @@ -357,6 +357,8 @@ const messages = { responseSubmitted: "Antwort gesendet", noFields: "Dieses Formular hat noch keine Felder.", failedSubmit: "Formular konnte nicht gesendet werden", + uncheckablePattern: + "Die Regel dieses Formulars für {label} kann nicht geprüft werden. Bitten Sie den Formularbesitzer, sie zu korrigieren.", }, responseInsights: { unavailable: "Insights nicht verfügbar", diff --git a/templates/forms/app/i18n/en-US.ts b/templates/forms/app/i18n/en-US.ts index 9ff0b3934cc..d5e102a4c51 100644 --- a/templates/forms/app/i18n/en-US.ts +++ b/templates/forms/app/i18n/en-US.ts @@ -374,6 +374,8 @@ const messages = { responseSubmitted: "Response submitted", noFields: "This form has no fields yet.", failedSubmit: "Failed to submit form", + uncheckablePattern: + "This form's rule for {label} can't be checked. Ask the form owner to fix it.", }, responseInsights: { unavailable: "Insights unavailable", diff --git a/templates/forms/app/i18n/es-ES.ts b/templates/forms/app/i18n/es-ES.ts index 39b4a2b995a..2c2d3af5fcb 100644 --- a/templates/forms/app/i18n/es-ES.ts +++ b/templates/forms/app/i18n/es-ES.ts @@ -358,6 +358,8 @@ const messages = { responseSubmitted: "Respuesta enviada", noFields: "Este formulario aún no tiene campos.", failedSubmit: "No se pudo enviar el formulario", + uncheckablePattern: + "La regla de este formulario para {label} no se puede comprobar. Pide al propietario del formulario que la corrija.", }, responseInsights: { unavailable: "Insights no disponibles", diff --git a/templates/forms/app/i18n/fr-FR.ts b/templates/forms/app/i18n/fr-FR.ts index be6af42839c..a0320a42908 100644 --- a/templates/forms/app/i18n/fr-FR.ts +++ b/templates/forms/app/i18n/fr-FR.ts @@ -359,6 +359,8 @@ const messages = { responseSubmitted: "Réponse envoyée", noFields: "Ce formulaire n’a pas encore de champs.", failedSubmit: "Impossible d’envoyer le formulaire", + uncheckablePattern: + "La règle de ce formulaire pour {label} ne peut pas être vérifiée. Demandez au propriétaire du formulaire de la corriger.", }, responseInsights: { unavailable: "Insights indisponibles", diff --git a/templates/forms/app/i18n/hi-IN.ts b/templates/forms/app/i18n/hi-IN.ts index 0a417fcacbf..ccd28728525 100644 --- a/templates/forms/app/i18n/hi-IN.ts +++ b/templates/forms/app/i18n/hi-IN.ts @@ -347,6 +347,8 @@ const messages = { responseSubmitted: "जवाब सबमिट हुआ", noFields: "इस फॉर्म में अभी कोई फ़ील्ड नहीं है।", failedSubmit: "फॉर्म सबमिट करने में विफल", + uncheckablePattern: + "इस फ़ॉर्म में {label} का नियम जाँचा नहीं जा सकता। कृपया फ़ॉर्म स्वामी से इसे ठीक करने को कहें।", }, responseInsights: { unavailable: "इनसाइट उपलब्ध नहीं", diff --git a/templates/forms/app/i18n/ja-JP.ts b/templates/forms/app/i18n/ja-JP.ts index 3dc06284692..7ee6b6a6071 100644 --- a/templates/forms/app/i18n/ja-JP.ts +++ b/templates/forms/app/i18n/ja-JP.ts @@ -348,6 +348,8 @@ const messages = { responseSubmitted: "回答を送信しました", noFields: "このフォームにはまだフィールドがありません。", failedSubmit: "フォームを送信できませんでした", + uncheckablePattern: + "このフォームの「{label}」のルールは検証できません。フォームの所有者に修正を依頼してください。", }, responseInsights: { unavailable: "インサイトを利用できません", diff --git a/templates/forms/app/i18n/ko-KR.ts b/templates/forms/app/i18n/ko-KR.ts index 959318f4c2d..a372e0f6c29 100644 --- a/templates/forms/app/i18n/ko-KR.ts +++ b/templates/forms/app/i18n/ko-KR.ts @@ -345,6 +345,8 @@ const messages = { responseSubmitted: "응답이 제출됨", noFields: "이 양식에는 아직 필드가 없습니다.", failedSubmit: "양식을 제출하지 못했습니다", + uncheckablePattern: + "이 양식의 {label} 규칙을 확인할 수 없습니다. 양식 소유자에게 수정을 요청하세요.", }, responseInsights: { unavailable: "인사이트를 사용할 수 없음", diff --git a/templates/forms/app/i18n/pt-BR.ts b/templates/forms/app/i18n/pt-BR.ts index 7ede5850d37..6696b45bb48 100644 --- a/templates/forms/app/i18n/pt-BR.ts +++ b/templates/forms/app/i18n/pt-BR.ts @@ -358,6 +358,8 @@ const messages = { responseSubmitted: "Resposta enviada", noFields: "Este formulário ainda não tem campos.", failedSubmit: "Falha ao enviar o formulário", + uncheckablePattern: + "A regra deste formulário para {label} não pode ser verificada. Peça ao proprietário do formulário para corrigi-la.", }, responseInsights: { unavailable: "Insights indisponíveis", diff --git a/templates/forms/app/i18n/zh-CN.ts b/templates/forms/app/i18n/zh-CN.ts index 7820807e0da..43fdc300fff 100644 --- a/templates/forms/app/i18n/zh-CN.ts +++ b/templates/forms/app/i18n/zh-CN.ts @@ -326,6 +326,8 @@ const messages = { responseSubmitted: "回复已提交", noFields: "此表单还没有字段。", failedSubmit: "提交表单失败", + uncheckablePattern: + "此表单中“{label}”的规则无法校验。请联系表单所有者修复。", }, responseInsights: { unavailable: "洞察不可用", diff --git a/templates/forms/app/i18n/zh-TW.ts b/templates/forms/app/i18n/zh-TW.ts index c703d9a89aa..8f898bf09cb 100644 --- a/templates/forms/app/i18n/zh-TW.ts +++ b/templates/forms/app/i18n/zh-TW.ts @@ -327,6 +327,8 @@ const messages = { responseSubmitted: "回覆已提交", noFields: "此表單還沒有欄位。", failedSubmit: "提交表單失敗", + uncheckablePattern: + "此表單中「{label}」的規則無法檢核。請聯絡表單擁有者修正。", }, responseInsights: { unavailable: "洞察不可用", diff --git a/templates/forms/app/pages/FormFillPage.tsx b/templates/forms/app/pages/FormFillPage.tsx index fc818ba0bcc..90592ec9042 100644 --- a/templates/forms/app/pages/FormFillPage.tsx +++ b/templates/forms/app/pages/FormFillPage.tsx @@ -1,6 +1,9 @@ import { useT } from "@agent-native/core/client/i18n"; import { Turnstile, PoweredByBadge } from "@agent-native/core/client/ui"; -import { normalizeDocumentTitle } from "@agent-native/core/shared"; +import { + normalizeDocumentTitle, + testUserRegex, +} from "@agent-native/core/shared"; import { isConditionalFieldVisible } from "@shared/conditional"; import { getFormCompletionMode, @@ -165,8 +168,11 @@ export function FormFillPage() { ); } if (field.validation.pattern && typeof val === "string") { - const regex = new RegExp(field.validation.pattern); - if (!regex.test(val)) { + const result = testUserRegex(field.validation.pattern, val); + if (result.status === "unevaluated") { + return t("publicForm.uncheckablePattern", { label: field.label }); + } + if (result.status === "no-match") { return field.validation.message || `${field.label} is invalid`; } } diff --git a/templates/forms/server/lib/public-form-ssr.ts b/templates/forms/server/lib/public-form-ssr.ts index 95cf8c21938..d088edd305b 100644 --- a/templates/forms/server/lib/public-form-ssr.ts +++ b/templates/forms/server/lib/public-form-ssr.ts @@ -7,6 +7,7 @@ import { AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, SSR_QUERY_CACHE_KEY_HEADER, + compileUserRegex, withAgentNativeSocialImageCacheBuster, } from "@agent-native/core/shared"; import { eq } from "drizzle-orm"; @@ -177,6 +178,30 @@ function normalizeOptions(options: unknown): string[] { * `javascript:fetch(...)` redirectUrl would execute attacker JS in the * form-publisher origin against any anonymous submitter. */ +/** Field validation as shipped to the public page: an unsafe `pattern` is + * removed and replaced by `unsafePattern` so the runtime can say so. */ +type PublicFieldValidation = Omit< + NonNullable, + "pattern" +> & { pattern?: string; unsafePattern?: true }; + +/** + * The inline runtime re-checks `validation.pattern` in the respondent browser, + * where nothing can abort a regex that backtracks exponentially. Decide safety + * here, where the analyzer lives, and ship the respondent either a pattern that + * is safe to run or an explicit "cannot check" marker, never a pattern that + * freezes their tab. + */ +export function publicValidation( + validation: FormField["validation"], +): PublicFieldValidation | undefined { + if (!validation) return undefined; + if (!validation.pattern) return validation; + if (compileUserRegex(validation.pattern).status === "ok") return validation; + const { pattern: _unsafe, ...rest } = validation; + return { ...rest, unsafePattern: true }; +} + export function safeRedirectUrl(value: unknown): string { if (typeof value !== "string") return ""; const trimmed = value.trim(); @@ -465,7 +490,7 @@ function renderFormPage( var COMPLETION_REFRESH_MS = ${completionRefreshMilliseconds}; var REDIRECT = ${JSON.stringify(safeRedirectUrl(settings.redirectUrl))}; var TURNSTILE_KEY = ${JSON.stringify(turnstileSiteKey)}; - var FIELDS = ${JSON.stringify(fields.map((f) => ({ id: f.id, type: f.type, required: f.required, validation: f.validation, label: f.label, conditional: f.conditional, multiple: f.multiple, accept: f.accept, maxSizeBytes: f.maxSizeBytes, maxFiles: f.maxFiles })))}; + var FIELDS = ${JSON.stringify(fields.map((f) => ({ id: f.id, type: f.type, required: f.required, validation: publicValidation(f.validation), label: f.label, conditional: f.conditional, multiple: f.multiple, accept: f.accept, maxSizeBytes: f.maxSizeBytes, maxFiles: f.maxFiles })))}; var SENSITIVE_QUERY_PARAMS = ${JSON.stringify(SENSITIVE_QUERY_PARAMS)}; function scrubPageUrl(value) { @@ -702,7 +727,9 @@ function renderFormPage( return (f.validation.message || f.label + " must be at least " + f.validation.min); if (f.validation.max != null && Number(v) > f.validation.max) return (f.validation.message || f.label + " must be at most " + f.validation.max); - if (f.validation.pattern && typeof v === "string" && !new RegExp(f.validation.pattern).test(v)) + if (f.validation.unsafePattern) + return f.label + " has a validation rule that cannot be checked. Ask the form owner to fix it."; + if (f.validation.pattern && typeof v === "string" && v.length <= 4096 && !new RegExp(f.validation.pattern).test(v)) return (f.validation.message || f.label + " is invalid"); } } diff --git a/templates/forms/server/lib/submission-validation.ts b/templates/forms/server/lib/submission-validation.ts index 930e87ad8d3..a13cbf3cebb 100644 --- a/templates/forms/server/lib/submission-validation.ts +++ b/templates/forms/server/lib/submission-validation.ts @@ -1,4 +1,5 @@ import { isAllowedUploadMimeType } from "@agent-native/core/server"; +import { testUserRegex } from "@agent-native/core/shared"; import type { FormField, FormFileValue } from "../../shared/types.js"; import { @@ -84,12 +85,16 @@ function isAbsentSubmissionValue(value: unknown): boolean { function validatePattern(field: FormField, value: string): string | null { const pattern = field.validation?.pattern; if (!pattern) return null; - try { - if (!new RegExp(pattern).test(value)) { - return field.validation?.message || `${fieldLabel(field)} is invalid`; - } - } catch { - return `${fieldLabel(field)} has an invalid validation pattern`; + // Forms authored before the authoring gate landed can still hold a pattern + // that backtracks exponentially, and this runs inside the submit handler — + // evaluating one would peg the event loop for every other request too. + // Refuse the submission rather than silently accepting an unenforced rule. + const result = testUserRegex(pattern, value); + if (result.status === "unevaluated") { + return `${fieldLabel(field)} has a validation pattern that cannot be checked safely: ${result.reason}`; + } + if (result.status === "no-match") { + return field.validation?.message || `${fieldLabel(field)} is invalid`; } return null; } diff --git a/templates/forms/server/lib/validate-fields.ts b/templates/forms/server/lib/validate-fields.ts index aa2eca74b56..aa9f55db889 100644 --- a/templates/forms/server/lib/validate-fields.ts +++ b/templates/forms/server/lib/validate-fields.ts @@ -3,6 +3,8 @@ // by the public form SSR renderer and into CSS/JS selectors by the inline // runtime — an unrestricted id like `x" onfocus="alert(1)` would otherwise // stored-XSS every anonymous submitter of a published form. +import { compileUserRegex } from "@agent-native/core/shared"; + import { DEFAULT_FORM_FILE_MAX_BYTES, isValidFileAccept, @@ -222,13 +224,26 @@ export function assertValidFields(fields: unknown): void { `field #${idx + 1} validation.pattern must be a string`, ); } - try { - new RegExp(v.pattern); - } catch { + // A syntactically valid pattern is not a safe one. `^([A-Za-z]+\s?)+$` + // — what an LLM reaches for to mean "at least two words" — backtracks + // exponentially and freezes both the respondent's tab and the submit + // handler's event loop. Reject it here so it never reaches the column. + const compiled = compileUserRegex(v.pattern); + if (compiled.status === "invalid-syntax") { throw new Error( `field #${idx + 1} validation.pattern must be a valid regular expression`, ); } + if (compiled.status === "too-long") { + throw new Error( + `field #${idx + 1} validation.pattern is too long: ${compiled.message}`, + ); + } + if (compiled.status === "unsafe") { + throw new Error( + `field #${idx + 1} validation.pattern can hang the browser and the server: ${compiled.message}. Rewrite it without overlapping repetition — for example use \`^\\S+(\\s+\\S+)+$\` for "at least two words".`, + ); + } } } } diff --git a/templates/forms/server/lib/validation-pattern-redos.spec.ts b/templates/forms/server/lib/validation-pattern-redos.spec.ts new file mode 100644 index 00000000000..d622a8907b0 --- /dev/null +++ b/templates/forms/server/lib/validation-pattern-redos.spec.ts @@ -0,0 +1,116 @@ +/** + * Regression coverage for the reported hang: an agent asked to "ensure Full + * Name accepts at least two words" wrote `^([A-Za-z]+\s?)+$`, the editor tab + * stopped responding, and Chrome offered to kill the page. + * + * Every assertion in this file is time-bounded on purpose. Before the fix the + * pattern below did not fail these tests, it hung them: a single + * `new RegExp(source).test(value)` on a 58-character value runs for hours. + */ +import { describe, expect, it } from "vitest"; + +import type { FormField } from "../../shared/types.js"; +import { publicValidation } from "./public-form-ssr.js"; +import { validateSubmissionField } from "./submission-validation.js"; +import { assertValidFields } from "./validate-fields.js"; + +/** Verbatim from the report. 17 characters, compiles fine, never returns. */ +const REPORTED_PATTERN = "^([A-Za-z]+\\s?)+$"; + +/** The rule the user actually wanted, which must keep working. */ +const SAFE_TWO_WORDS = "^\\S+(\\s+\\S+)+$"; + +/** Long enough that the reported pattern would outlive the test run. */ +const HOSTILE_VALUE = "Jonathan Alexander Montgomery Wellington Smith Junior!"; + +function fullNameField(pattern: string): FormField { + return { + id: "full-name", + type: "text", + label: "Full Name", + required: true, + validation: { pattern }, + } as FormField; +} + +/** Fails loudly if the body blocks, instead of hanging the whole suite. */ +function withinBudget(budgetMs: number, body: () => T): T { + const started = Date.now(); + const result = body(); + const elapsed = Date.now() - started; + expect(elapsed).toBeLessThan(budgetMs); + return result; +} + +describe("agent-authored validation patterns", () => { + it("refuses to store the pattern that froze the editor tab", () => { + withinBudget(1000, () => { + expect(() => + assertValidFields([fullNameField(REPORTED_PATTERN)]), + ).toThrow(/can hang the browser and the server/i); + }); + }); + + it("names a safe alternative so the agent can fix its own rule", () => { + let message = ""; + try { + assertValidFields([fullNameField(REPORTED_PATTERN)]); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + expect(message).toContain("at least two words"); + expect(message).toContain("\\S+(\\s+\\S+)+"); + }); + + it("still accepts the correct two-word rule and enforces it", () => { + const field = fullNameField(SAFE_TWO_WORDS); + expect(() => assertValidFields([field])).not.toThrow(); + expect(validateSubmissionField(field, "Ada Lovelace")).toBeNull(); + expect(validateSubmissionField(field, "Ada")).toBe("Full Name is invalid"); + }); + + it("still accepts an ordinary pattern with no repetition ambiguity", () => { + const field = fullNameField("^[A-Za-z ]{3,64}$"); + expect(() => assertValidFields([field])).not.toThrow(); + expect(validateSubmissionField(field, "Ada Lovelace")).toBeNull(); + }); + + it("rejects a syntactically broken pattern with its own message", () => { + expect(() => assertValidFields([fullNameField("^[a-")])).toThrow( + /must be a valid regular expression/i, + ); + }); + + it("does not hang the submit handler on a form saved before the gate", () => { + // Forms already in the database still carry the poisoned pattern, so the + // execution site has to bound itself rather than trust the authoring gate. + const error = withinBudget(1000, () => + validateSubmissionField(fullNameField(REPORTED_PATTERN), HOSTILE_VALUE), + ); + expect(error).toMatch(/cannot be checked safely/i); + }); + + it("never reports an unrunnable rule as a passing one", () => { + // The dangerous silent failure is the opposite of a hang: accepting the + // submission because the rule could not be evaluated. + const error = validateSubmissionField( + fullNameField(REPORTED_PATTERN), + "Ada Lovelace", + ); + expect(error).not.toBeNull(); + }); + + it("does not ship the poisoned pattern to the public form runtime", () => { + const shipped = withinBudget(1000, () => + publicValidation({ pattern: REPORTED_PATTERN }), + ); + expect(shipped?.pattern).toBeUndefined(); + expect(shipped?.unsafePattern).toBe(true); + }); + + it("ships a safe pattern to the public form runtime unchanged", () => { + const shipped = publicValidation({ pattern: SAFE_TWO_WORDS }); + expect(shipped?.pattern).toBe(SAFE_TWO_WORDS); + expect(shipped?.unsafePattern).toBeUndefined(); + }); +}); diff --git a/templates/forms/shared/field-schema.ts b/templates/forms/shared/field-schema.ts index b9a626f1ad7..dec2d435377 100644 --- a/templates/forms/shared/field-schema.ts +++ b/templates/forms/shared/field-schema.ts @@ -77,7 +77,9 @@ export const formFieldSchema = z pattern: z .string() .optional() - .describe("Regular expression the value must match (text, email)."), + .describe( + "Regular expression the value must match (text, email). Rejected if it can backtrack catastrophically, which would freeze the respondent's browser and the submit handler. Avoid a repeated group whose body is itself optional or repeated: write `^\\S+(\\s+\\S+)+$` for 'at least two words', never `^([A-Za-z]+\\s?)+$`.", + ), message: z .string() .optional() diff --git a/templates/slides/server/lib/slide-content-patch.ts b/templates/slides/server/lib/slide-content-patch.ts index 50c063b7c2c..786fb180940 100644 --- a/templates/slides/server/lib/slide-content-patch.ts +++ b/templates/slides/server/lib/slide-content-patch.ts @@ -1,5 +1,6 @@ import { applyTargetedReplace, + analyzeRegexSource, findTargetedMatches, wrapDiagnosticSnippet, type TargetedAmbiguousMatch, @@ -648,6 +649,15 @@ function applyRegexReplace( edit: Extract, ): { content: string; summary: string } { const flags = normalizeRegexFlags(edit.flags, edit.all); + // `matchAll` over slide HTML is unbounded work for a pattern that backtracks + // exponentially, and nothing can interrupt it once V8 is inside the match. + // Name the mistake so the agent rewrites the pattern instead of retrying it. + const verdict = analyzeRegexSource(edit.pattern); + if (!verdict.safe) { + throw new SlideContentEditError( + `regex-replace pattern cannot be run safely: ${verdict.reason}. Rewrite it without overlapping repetition, or use a \`find\` edit instead.`, + ); + } const regex = new RegExp(edit.pattern, flags); const countRegex = new RegExp(edit.pattern, ensureGlobal(flags)); const matches = Array.from(content.matchAll(countRegex)).length; From e9dca1faad14252f51be6c20e7091925bb9d22e2 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 15 Sep 2026 14:01:34 +0000 Subject: [PATCH 2/5] fix(core): close analyzer gaps found in review Six false negatives, each runtime-validated as catastrophic before fixing: - The probe alphabet was a fixed list, so any pattern over characters it omitted produced empty character sets and read as unambiguous. `^(A+)+$` and `^(x|x)+$` were cleared; `^(a+)+$` only failed because "a" happened to be in the list. The alphabet is now derived from the pattern itself, and an atom the probes cannot describe is "unknown" and overlaps everything rather than being treated as disjoint. - Flags are now part of the verdict. `^(a|A)+$` is unambiguous on its own and catastrophic under `i`; the Slides regex-replace op analyzed the source while running it with flags. - Ambiguity checks keyed on infinite quantifiers only, so `^(a{1,10})+$` evaded them. They now key on variable length. - Overlapping alternatives were only compared when both sides were a single atom, clearing `^(a|aa)+$`. They are now compared on leading characters and minimum length, which still admits `(cat|car)+`. - Three chained repetitions over the same characters backtrack cubically and exceed the input cap: `^(a+)(a+)(a+)$` takes over 20s at 4096 characters. Runs of three or more are now rejected; pairs are quadratic and stay admitted, so the standard email pattern is unaffected. - The public form runtime skipped the pattern check for values over the cap, so an unchecked value looked valid in the browser while the server rejected it. It now reports the value as unchecked. Corpus expanded to 16 catastrophic and 17 legitimate patterns; every cleared pattern verified at 0 ms against adversarial input at the full 4096-char cap. --- .../core/src/shared/bounded-regex.spec.ts | 40 ++++ packages/core/src/shared/bounded-regex.ts | 212 ++++++++++++++---- templates/forms/server/lib/public-form-ssr.ts | 9 +- .../slides/server/lib/slide-content-patch.ts | 4 +- 4 files changed, 219 insertions(+), 46 deletions(-) diff --git a/packages/core/src/shared/bounded-regex.spec.ts b/packages/core/src/shared/bounded-regex.spec.ts index 51bbe9b0a1e..8e450d8b867 100644 --- a/packages/core/src/shared/bounded-regex.spec.ts +++ b/packages/core/src/shared/bounded-regex.spec.ts @@ -22,6 +22,19 @@ const CATASTROPHIC = [ "^(a|a)*$", "^(\\d|\\w)+$", "^(\\s*\\S+)*$", + // Letters outside the baseline probe alphabet. These read as unambiguous + // while the analyzer only probed a fixed character list, so the corpus above + // passed while `^(A+)+$` still hung. + "^(A+)+$", + "^(Q+)+$", + "^(x|x)+$", + // Overlapping alternatives of differing length. + "^(a|aa)+$", + // Finite inner quantifier: bounded is not the same as unambiguous. + "^(a{1,10})+$", + // Three chained repetitions over the same characters: cubic, and over 20 + // seconds at the input cap even though no single group is ambiguous. + "^(a+)(a+)(a+)$", ]; /** Patterns that must keep working — including correct "two words" rules. */ @@ -38,6 +51,11 @@ const LINEAR = [ "^https?://\\S+$", "^.{8,64}$", "^(?:Mr|Mrs|Ms|Dr)\\.? [A-Za-z]+$", + // Disjoint case-sensitively, and only dangerous once `i` is applied. + "^(a|A)+$", + "^#[0-9a-fA-F]{6}$", + "^[A-Z]{3}-[0-9]{4}$", + "^\\S+@\\S+\\.\\S+$", ]; /** Long enough that an exponential pattern would not return this decade. */ @@ -55,6 +73,28 @@ describe("analyzeRegexSource", () => { expect(analyzeRegexSource(source)).toEqual({ safe: true }); }); + it("folds case when the pattern will run with the i flag", () => { + // Same source, opposite verdicts. Analyzing without the caller's flags + // answers a different question than the one that gets executed. + expect(analyzeRegexSource("^(a|A)+$", "").safe).toBe(true); + expect(analyzeRegexSource("^(a|A)+$", "i").safe).toBe(false); + expect(analyzeRegexSource("^([a-z]|[A-Z])+$", "i").safe).toBe(false); + }); + + it("fails closed on a construct it cannot characterize", () => { + // A backreference cannot be reduced to a character set, so it must not be + // reported as provably disjoint from its neighbour. + expect(analyzeRegexSource("^(\\w)(\\1+)+$").safe).toBe(false); + }); + + it("keeps a single overlapping pair, which is only quadratic", () => { + // Two chained repetitions stay inside the input cap; only three or more + // exceed it. Rejecting pairs would take the standard email pattern with it. + expect(analyzeRegexSource("^[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$").safe).toBe( + true, + ); + }); + it("refuses to clear a pattern it cannot parse", () => { expect(analyzeRegexSource("^(unclosed").safe).toBe(false); }); diff --git a/packages/core/src/shared/bounded-regex.ts b/packages/core/src/shared/bounded-regex.ts index c092af5d004..5e943304f85 100644 --- a/packages/core/src/shared/bounded-regex.ts +++ b/packages/core/src/shared/bounded-regex.ts @@ -32,11 +32,12 @@ export const MAX_USER_REGEX_INPUT_LENGTH = 4096; const MAX_GROUP_DEPTH = 12; /** - * Representative characters used to approximate "can these two atoms match the - * same character". A fixed probe alphabet keeps the comparison cheap and lets - * the engine itself answer the question for each single character. + * Baseline probe characters, covering the common shorthand classes. This set + * alone is not enough: a pattern over letters this list happens to omit (`A`, + * `x`) would produce empty character sets and read as unambiguous, so + * `collectProbeChars` adds every character the pattern itself names. */ -const PROBE_CHARS = [ +const BASE_PROBE_CHARS = [ "a", "Z", "5", @@ -54,6 +55,31 @@ const PROBE_CHARS = [ "!", ]; +/** Escapes that denote a class or assertion rather than a literal character. */ +const NON_LITERAL_ESCAPES = new Set("dDwWsSbBnrtfvxucpPk0123456789".split("")); + +/** + * Probe alphabet for one pattern: the baseline plus every literal character the + * pattern mentions, including character-class members and range endpoints. Two + * atoms can only be compared on characters the probe set actually contains, so + * anything the pattern names has to be in it. + */ +function collectProbeChars(source: string): string[] { + const chars = new Set(BASE_PROBE_CHARS); + for (let i = 0; i < source.length; i += 1) { + const ch = source[i]; + if (ch === "\\") { + const next = source[i + 1]; + if (next && !NON_LITERAL_ESCAPES.has(next)) chars.add(next); + i += 1; + continue; + } + if ("()[]{}|^$.*+?".includes(ch)) continue; + chars.add(ch); + } + return [...chars]; +} + type AtomKind = | "group" | "class" @@ -245,47 +271,94 @@ function isUnbounded(atom: RegexAtom): boolean { } /** - * Characters this atom can match in a single position. Derived by asking the - * engine itself, one probe character at a time, so a single-character match can - * never be the expensive case. + * Whether this atom can consume different numbers of characters. `{1,10}` is + * finite but still ambiguous under an outer repetition, so a bounded quantifier + * is not automatically safe. */ -function charSetOf(atom: RegexAtom): Set { - if (atom.kind === "anchor" || atom.kind === "backref") return new Set(); +function isVariableLength(atom: RegexAtom): boolean { + if (atom.max > atom.min) return true; + if (atom.kind !== "group") return false; + const branches = atom.branches ?? []; + if (branches.some((branch) => branch.some(isVariableLength))) return true; + const lengths = new Set(branches.map((branch) => consuming(branch).length)); + return lengths.size > 1; +} + +/** Smallest number of characters a branch must consume. */ +function minLength(branch: RegexAtom[]): number { + let total = 0; + for (const atom of consuming(branch)) { + total += + atom.kind === "group" && atom.branches?.length + ? atom.min * Math.min(...atom.branches.map(minLength)) + : atom.min; + } + return total; +} + +/** + * Characters an atom can match in one position, or `unknown` when the analyzer + * cannot characterize it. `unknown` overlaps everything: an atom we cannot read + * must not be reported as provably disjoint from its neighbour. + */ +type CharSet = Set | "unknown"; + +/** + * Derived by asking the engine itself, one probe character at a time, so a + * single-character match can never be the expensive case. The pattern flags + * matter here: under `i`, `a` and `A` are the same character, which is what + * turns an otherwise disjoint alternation into an ambiguous one. + */ +function charSetOf(atom: RegexAtom, ctx: AnalysisContext): CharSet { + if (atom.kind === "anchor") return new Set(); + if (atom.kind === "backref") return "unknown"; if (atom.kind === "group") { const set = new Set(); for (const branch of atom.branches ?? []) { - for (const ch of leadingCharSet(branch)) set.add(ch); + const leading = leadingCharSet(branch, ctx); + if (leading === "unknown") return "unknown"; + for (const ch of leading) set.add(ch); } return set; } - const set = new Set(); let probe: RegExp; try { - probe = new RegExp(`^(?:${atom.source})$`); + probe = new RegExp(`^(?:${atom.source})$`, ctx.flags); } catch { - return set; + return "unknown"; } - for (const ch of PROBE_CHARS) { + const set = new Set(); + for (const ch of ctx.probeChars) { if (probe.test(ch)) set.add(ch); } - return set; + // A consuming atom that matches none of the probes is one the probe alphabet + // cannot describe, not one that matches nothing. + return set.size === 0 ? "unknown" : set; } /** Characters a branch can start with, looking past nullable leading atoms. */ -function leadingCharSet(branch: RegexAtom[]): Set { +function leadingCharSet(branch: RegexAtom[], ctx: AnalysisContext): CharSet { const set = new Set(); for (const atom of branch) { - for (const ch of charSetOf(atom)) set.add(ch); + const atomSet = charSetOf(atom, ctx); + if (atomSet === "unknown") return "unknown"; + for (const ch of atomSet) set.add(ch); if (!isNullable(atom)) break; } return set; } -function overlaps(a: Set, b: Set): boolean { +function overlaps(a: CharSet, b: CharSet): boolean { + if (a === "unknown" || b === "unknown") return true; for (const ch of a) if (b.has(ch)) return true; return false; } +interface AnalysisContext { + flags: string; + probeChars: readonly string[]; +} + /** Atoms that actually consume input — anchors carry no matching cost. */ function consuming(branch: RegexAtom[]): RegexAtom[] { return branch.filter((atom) => atom.kind !== "anchor"); @@ -305,22 +378,25 @@ function describe(atom: RegexAtom): string { return `${atom.source}${quantifier}`; } -function analyzeRepeatedGroup(atom: RegexAtom): string | null { +function analyzeRepeatedGroup( + atom: RegexAtom, + ctx: AnalysisContext, +): string | null { const branches = atom.branches ?? []; for (const branch of branches) { const atoms = consuming(branch); if (atoms.length === 0) continue; - // An inner repetition that may match nothing lets the outer repetition - // split the same input in exponentially many ways. + // An inner part that may match nothing lets the outer repetition split the + // same input in exponentially many ways. if (atoms.length > 1) { const nullable = atoms.find(isNullable); - const unbounded = atoms.find( - (candidate) => isUnbounded(candidate) && candidate !== nullable, + const variable = atoms.find( + (candidate) => isVariableLength(candidate) && candidate !== nullable, ); - if (nullable && unbounded) { - return `repeated group \`${describe(atom)}\` contains both an optional part (\`${describe(nullable)}\`) and an unbounded repetition (\`${describe(unbounded)}\`), so the same text can be split in exponentially many ways`; + if (nullable && variable) { + return `repeated group \`${describe(atom)}\` contains both an optional part (\`${describe(nullable)}\`) and a variable-length part (\`${describe(variable)}\`), so the same text can be split in exponentially many ways`; } } @@ -328,10 +404,8 @@ function analyzeRepeatedGroup(atom: RegexAtom): string | null { for (let i = 0; i + 1 < atoms.length; i += 1) { const left = atoms[i]; const right = atoms[i + 1]; - if (!isUnbounded(left) && !isUnbounded(right)) continue; - if (!isNullable(left) && !isNullable(right) && !isUnbounded(left)) - continue; - if (overlaps(charSetOf(left), charSetOf(right))) { + if (!isVariableLength(left) && !isVariableLength(right)) continue; + if (overlaps(charSetOf(left, ctx), charSetOf(right, ctx))) { return `repeated group \`${describe(atom)}\` has adjacent repetitions (\`${describe(left)}\` and \`${describe(right)}\`) that match the same characters`; } } @@ -340,8 +414,8 @@ function analyzeRepeatedGroup(atom: RegexAtom): string | null { const first = atoms[0]; const last = atoms[atoms.length - 1]; if ( - (isUnbounded(first) || isUnbounded(last)) && - overlaps(charSetOf(last), charSetOf(first)) + (isVariableLength(first) || isVariableLength(last)) && + overlaps(charSetOf(last, ctx), charSetOf(first, ctx)) ) { return `repeated group \`${describe(atom)}\` can match the same characters at the start and end of each repetition`; } @@ -351,14 +425,22 @@ function analyzeRepeatedGroup(atom: RegexAtom): string | null { } } - // Alternatives inside a repetition that accept the same single-atom input. + // Alternatives inside a repetition that can both claim the same text. Equal + // fixed-length alternatives that merely share a first character (`cat|car`) + // are unambiguous, so length has to differ or one side has to be able to + // stretch before this counts. for (let i = 0; i < branches.length; i += 1) { for (let j = i + 1; j < branches.length; j += 1) { - const a = consuming(branches[i]); - const b = consuming(branches[j]); - if (a.length !== 1 || b.length !== 1) continue; - if (overlaps(charSetOf(a[0]), charSetOf(b[0]))) { - return `repeated group \`${describe(atom)}\` has alternatives (\`${describe(a[0])}\` and \`${describe(b[0])}\`) that match the same characters`; + const a = branches[i]; + const b = branches[j]; + if (!overlaps(leadingCharSet(a, ctx), leadingCharSet(b, ctx))) continue; + const ambiguous = + minLength(a) !== minLength(b) || + a.some(isVariableLength) || + b.some(isVariableLength) || + (consuming(a).length === 1 && consuming(b).length === 1); + if (ambiguous) { + return `repeated group \`${describe(atom)}\` has alternatives that can match the same text in more than one way`; } } } @@ -366,15 +448,46 @@ function analyzeRepeatedGroup(atom: RegexAtom): string | null { return null; } -function walk(branches: RegexAtom[][]): string | null { +/** + * Three or more adjacent variable-length repetitions over the same characters + * backtrack cubically or worse, which exceeds the input cap even though no + * single group is ambiguous on its own: `^(a+)(a+)(a+)$` needs over 20 seconds + * at 4096 characters. Two adjacent overlapping repetitions are only quadratic + * and stay inside the budget, so the run has to reach three before this fires. + * Non-overlapping separators (the `@` in an email pattern) break the run, which + * is what keeps ordinary patterns out of this check. + */ +function analyzeAdjacentRun( + branch: RegexAtom[], + ctx: AnalysisContext, +): string | null { + const atoms = consuming(branch); + let run: RegexAtom[] = []; + for (const atom of atoms) { + const previous = run[run.length - 1]; + const continues = + isVariableLength(atom) && + (run.length === 0 || + overlaps(charSetOf(previous, ctx), charSetOf(atom, ctx))); + run = continues ? [...run, atom] : isVariableLength(atom) ? [atom] : []; + if (run.length >= 3) { + return `\`${run.map(describe).join("")}\` chains three repetitions over the same characters, which backtracks cubically`; + } + } + return null; +} + +function walk(branches: RegexAtom[][], ctx: AnalysisContext): string | null { for (const branch of branches) { + const chained = analyzeAdjacentRun(branch, ctx); + if (chained) return chained; for (const atom of branch) { if (atom.kind !== "group") continue; if (isUnbounded(atom)) { - const reason = analyzeRepeatedGroup(atom); + const reason = analyzeRepeatedGroup(atom, ctx); if (reason) return reason; } - const nested = walk(atom.branches ?? []); + const nested = walk(atom.branches ?? [], ctx); if (nested) return nested; } } @@ -385,8 +498,15 @@ function walk(branches: RegexAtom[][]): string | null { * Report whether `source` is shaped like a pattern that can backtrack * super-linearly. A `safe: true` verdict means no known blowup signature was * found, not that the pattern is provably linear. + * + * `flags` participates in the verdict. `^(a|A)+$` is unambiguous on its own and + * catastrophic under `i`, so analyzing the source without the flags it will be + * run with answers a different question than the caller asked. */ -export function analyzeRegexSource(source: string): RegexSafetyVerdict { +export function analyzeRegexSource( + source: string, + flags = "", +): RegexSafetyVerdict { const state: ParseState = { source, index: 0, depth: 0, bailed: false }; const branches = parseAlternation(state); if (state.bailed || state.index < source.length) { @@ -396,7 +516,13 @@ export function analyzeRegexSource(source: string): RegexSafetyVerdict { "pattern uses constructs this validator cannot analyze for catastrophic backtracking", }; } - const reason = walk(branches); + // Only case folding changes which characters two atoms share; the rest affect + // anchoring or iteration, and `y`/`g` would break the single-character probes. + const probeFlags = flags.includes("i") ? "i" : ""; + const reason = walk(branches, { + flags: probeFlags, + probeChars: collectProbeChars(source), + }); return reason ? { safe: false, reason } : { safe: true }; } @@ -431,7 +557,7 @@ export function compileUserRegex( }; } - const verdict = analyzeRegexSource(source); + const verdict = analyzeRegexSource(source, options.flags); if (!verdict.safe) { return { status: "unsafe", message: verdict.reason }; } diff --git a/templates/forms/server/lib/public-form-ssr.ts b/templates/forms/server/lib/public-form-ssr.ts index d088edd305b..bb53b99627a 100644 --- a/templates/forms/server/lib/public-form-ssr.ts +++ b/templates/forms/server/lib/public-form-ssr.ts @@ -7,6 +7,7 @@ import { AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, SSR_QUERY_CACHE_KEY_HEADER, + MAX_USER_REGEX_INPUT_LENGTH, compileUserRegex, withAgentNativeSocialImageCacheBuster, } from "@agent-native/core/shared"; @@ -729,8 +730,12 @@ function renderFormPage( return (f.validation.message || f.label + " must be at most " + f.validation.max); if (f.validation.unsafePattern) return f.label + " has a validation rule that cannot be checked. Ask the form owner to fix it."; - if (f.validation.pattern && typeof v === "string" && v.length <= 4096 && !new RegExp(f.validation.pattern).test(v)) - return (f.validation.message || f.label + " is invalid"); + if (f.validation.pattern && typeof v === "string") { + if (v.length > ${MAX_USER_REGEX_INPUT_LENGTH}) + return f.label + " is too long to check against this form's rule."; + if (!new RegExp(f.validation.pattern).test(v)) + return (f.validation.message || f.label + " is invalid"); + } } } return null; diff --git a/templates/slides/server/lib/slide-content-patch.ts b/templates/slides/server/lib/slide-content-patch.ts index 786fb180940..2a193a2490e 100644 --- a/templates/slides/server/lib/slide-content-patch.ts +++ b/templates/slides/server/lib/slide-content-patch.ts @@ -652,7 +652,9 @@ function applyRegexReplace( // `matchAll` over slide HTML is unbounded work for a pattern that backtracks // exponentially, and nothing can interrupt it once V8 is inside the match. // Name the mistake so the agent rewrites the pattern instead of retrying it. - const verdict = analyzeRegexSource(edit.pattern); + // The flags are part of the verdict: `^(a|A)+$` is unambiguous on its own and + // catastrophic under `i`. + const verdict = analyzeRegexSource(edit.pattern, flags); if (!verdict.safe) { throw new SlideContentEditError( `regex-replace pattern cannot be run safely: ${verdict.reason}. Rewrite it without overlapping repetition, or use a \`find\` edit instead.`, From 15f425c9c3dc2ba1a61f38024eb980f46c46638c Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 15 Sep 2026 14:47:50 +0000 Subject: [PATCH 3/5] chore: add the missing trailing newline lint requires `pnpm fmt:check` fails on main for this changelog file, which blocks every PR that merges it. Whitespace only. --- ...otion-from-a-document-now-opens-the-integrations-settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/content/changelog/2026-09-12-set-up-notion-from-a-document-now-opens-the-integrations-settings.md b/templates/content/changelog/2026-09-12-set-up-notion-from-a-document-now-opens-the-integrations-settings.md index 77bee06a809..b23038379ec 100644 --- a/templates/content/changelog/2026-09-12-set-up-notion-from-a-document-now-opens-the-integrations-settings.md +++ b/templates/content/changelog/2026-09-12-set-up-notion-from-a-document-now-opens-the-integrations-settings.md @@ -3,4 +3,4 @@ type: fixed date: 2026-09-12 --- -Setting up Notion from a document now opens the Integrations settings, where Notion can actually be connected. \ No newline at end of file +Setting up Notion from a document now opens the Integrations settings, where Notion can actually be connected. From f59bdc5376eb0efe88f1395f6aadc560d682d14e Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 15 Sep 2026 19:57:41 +0000 Subject: [PATCH 4/5] Close the second round of regex-analyzer gaps - analyze any group that can iterate more than once, not only unbounded ones: ^(a+){10}$ never returns on a 41-character non-match - flag duplicate alternatives inside a repeated group (^(ab|ab)+$) - carry s/u/v into the character-set probes alongside i, so ^(s|s)+Z$ under iu and ^(.|\n)+Z$ under s are no longer analyzed away - parse \p{...} as one atom instead of an escape plus a literal - cap and memoize the analysis itself; it is super-linear in the pattern length and Slides reached it without compileUserRegex's cap - exclude lookarounds from consuming-atom rules and skip bodies that can only take one character, which were rejecting the standard password rule and an IBAN pattern Scope the Forms authoring gate to write paths: the submit and upload handlers re-validate stored fields, where refusing the configuration replaces the field-level reason with a generic 500. Skip pattern checks for absent values in the SSR runtime and the React fill page, matching what the submit handler has always done. --- .../core/src/shared/bounded-regex.spec.ts | 57 +++++++ packages/core/src/shared/bounded-regex.ts | 146 ++++++++++++++++-- templates/forms/app/pages/FormFillPage.tsx | 5 +- .../forms/server/handlers/submissions.ts | 5 +- templates/forms/server/handlers/uploads.ts | 4 +- templates/forms/server/lib/public-form-ssr.ts | 8 +- templates/forms/server/lib/validate-fields.ts | 19 ++- .../lib/validation-pattern-redos.spec.ts | 26 ++++ .../server/lib/slide-content-patch.test.ts | 41 +++++ 9 files changed, 286 insertions(+), 25 deletions(-) diff --git a/packages/core/src/shared/bounded-regex.spec.ts b/packages/core/src/shared/bounded-regex.spec.ts index 8e450d8b867..ad20e7b6bd5 100644 --- a/packages/core/src/shared/bounded-regex.spec.ts +++ b/packages/core/src/shared/bounded-regex.spec.ts @@ -35,6 +35,13 @@ const CATASTROPHIC = [ // Three chained repetitions over the same characters: cubic, and over 20 // seconds at the input cap even though no single group is ambiguous. "^(a+)(a+)(a+)$", + // A finite outer repeat still re-splits the input across its iterations. + // This one does not return on a 41-character non-match. + "^(a+){10}$", + // Duplicate multi-character alternatives: an indistinguishable choice on + // every iteration, the same fan-out as `(a|a)+` without being single atoms. + "^(ab|ab)+$", + "^(abc|abc|x)+$", ]; /** Patterns that must keep working — including correct "two words" rules. */ @@ -56,6 +63,20 @@ const LINEAR = [ "^#[0-9a-fA-F]{6}$", "^[A-Z]{3}-[0-9]{4}$", "^\\S+@\\S+\\.\\S+$", + // A finite repeat of an unambiguous body is fine — the group has only one + // way to split each iteration, so checking `max > 1` must not reject it. + "^(\\d{2}){3}$", + "^([A-Z]-)+\\d$", + // Equal-length alternatives that differ somewhere: one way to match, not two. + "^(ab|ac)+$", + "^(GET|PUT)$", + // Lookarounds match a position, not text, so the standard password rule is + // three assertions and one repetition, not four competing repetitions. + "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$", + // A body that can only take one character has nothing to hand back and forth + // between iterations. Both measure 0 ms on a non-matching 41-character value. + "^(a?)+$", + "^[A-Z]{2}\\d{2}[A-Z0-9]{4}\\d{7}([A-Z0-9]?){0,16}$", ]; /** Long enough that an exponential pattern would not return this decade. */ @@ -81,6 +102,42 @@ describe("analyzeRegexSource", () => { expect(analyzeRegexSource("^([a-z]|[A-Z])+$", "i").safe).toBe(false); }); + it("carries dotAll and unicode into the character-set probes", () => { + // Slides runs a regex-replace with the agent's own flags. `.` overlaps the + // alternative only under `s` (7s on a 29-character non-match), and `ſ` + // folds onto `s` only under `iu` (2s on 25 characters). Analyzing without + // those flags clears both. + expect(analyzeRegexSource("^(.|\\n)+Z$", "").safe).toBe(true); + expect(analyzeRegexSource("^(.|\\n)+Z$", "s").safe).toBe(false); + expect(analyzeRegexSource("^(ſ|s)+Z$", "i").safe).toBe(true); + expect(analyzeRegexSource("^(ſ|s)+Z$", "iu").safe).toBe(false); + }); + + it("reads a unicode property escape as one atom", () => { + // `\p{L}` parsed as `\p` plus a literal `{L}` leaves a stray `}` holding + // the quantifier, so the verdict describes a pattern nobody wrote. + expect(analyzeRegexSource("^\\p{L}+$", "u")).toEqual({ safe: true }); + expect(analyzeRegexSource("^\\p{L}+ \\p{L}+$", "u")).toEqual({ + safe: true, + }); + const nested = analyzeRegexSource("^(\\p{L}+)+$", "u"); + expect(nested.safe).toBe(false); + if (!nested.safe) expect(nested.reason).toContain("\\p{L}"); + }); + + it("refuses a pattern too long to analyze cheaply", () => { + // The pair-wise alternative comparison is super-linear in the source + // length: 800 branches took 15s before this cap. Callers that reach the + // analyzer directly must not be able to trade a slow match for a slow + // verdict. + const branches = Array.from({ length: 400 }, (_, i) => `a${i}`).join("|"); + const source = `^(${branches})+$`; + expect(source.length).toBeGreaterThan(MAX_USER_REGEX_LENGTH); + const started = Date.now(); + expect(analyzeRegexSource(source).safe).toBe(false); + expect(Date.now() - started).toBeLessThan(100); + }); + it("fails closed on a construct it cannot characterize", () => { // A backreference cannot be reduced to a character set, so it must not be // reported as provably disjoint from its neighbour. diff --git a/packages/core/src/shared/bounded-regex.ts b/packages/core/src/shared/bounded-regex.ts index 5e943304f85..3c6a59b011b 100644 --- a/packages/core/src/shared/bounded-regex.ts +++ b/packages/core/src/shared/bounded-regex.ts @@ -31,6 +31,13 @@ export const MAX_USER_REGEX_INPUT_LENGTH = 4096; /** Nesting depth beyond which the analyzer stops trusting its own reading. */ const MAX_GROUP_DEPTH = 12; +/** + * Flags that change which characters an atom matches, and so have to reach the + * probes. `m` and `d` cannot affect a single-character membership test, and + * `g`/`y` would break it outright by carrying `lastIndex` between probes. + */ +const MATCHING_FLAGS = ["i", "s", "u", "v"]; + /** * Baseline probe characters, covering the common shorthand classes. This set * alone is not enough: a pattern over letters this list happens to omit (`A`, @@ -95,6 +102,8 @@ interface RegexAtom { source: string; /** Present for groups: the alternation branches of the group body. */ branches?: RegexAtom[][]; + /** Lookarounds match a position, not text, so they cost no input. */ + zeroWidth?: boolean; min: number; /** `Number.POSITIVE_INFINITY` for `*`, `+` and `{n,}`. */ max: number; @@ -180,6 +189,7 @@ function parseSequence(state: ParseState): RegexAtom[] { let kind: AtomKind = "literal"; let source = ""; let branches: RegexAtom[][] | undefined; + let zeroWidth = false; if (ch === "(") { if (state.depth >= MAX_GROUP_DEPTH) { @@ -198,9 +208,11 @@ function parseSequence(state: ParseState): RegexAtom[] { ) { state.index += 2; capturing = false; + zeroWidth = !marker.startsWith("?:"); } else if (marker === "?<=" || marker === "?", state.index); if (close === -1) { @@ -225,9 +237,27 @@ function parseSequence(state: ParseState): RegexAtom[] { source = parseCharClass(state); kind = "class"; } else if (ch === "\\") { - source = state.source.slice(state.index, state.index + 2); - state.index += 2; - kind = /^\\\d$/.test(source) ? "backref" : "escape"; + const next = state.source[state.index + 1]; + // `\p{L}` is one atom. Reading it as `\p` plus a literal `{L}` both + // misjudges its character set and leaves a stray `}` to carry the + // quantifier, so the verdict then describes a pattern nobody wrote. + if ( + (next === "p" || next === "P") && + state.source[state.index + 2] === "{" + ) { + const close = state.source.indexOf("}", state.index + 2); + if (close === -1) { + state.bailed = true; + break; + } + source = state.source.slice(state.index, close + 1); + state.index = close + 1; + kind = "class"; + } else { + source = state.source.slice(state.index, state.index + 2); + state.index += 2; + kind = /^\\\d$/.test(source) ? "backref" : "escape"; + } } else if (ch === ".") { state.index += 1; source = "."; @@ -243,7 +273,7 @@ function parseSequence(state: ParseState): RegexAtom[] { } const { min, max } = parseQuantifier(state); - atoms.push({ kind, source, branches, min, max }); + atoms.push({ kind, source, branches, zeroWidth, min, max }); } return atoms; } @@ -266,10 +296,6 @@ function isNullable(atom: RegexAtom): boolean { return false; } -function isUnbounded(atom: RegexAtom): boolean { - return atom.max === Number.POSITIVE_INFINITY; -} - /** * Whether this atom can consume different numbers of characters. `{1,10}` is * finite but still ambiguous under an outer repetition, so a bounded quantifier @@ -310,6 +336,14 @@ type CharSet = Set | "unknown"; * turns an otherwise disjoint alternation into an ambiguous one. */ function charSetOf(atom: RegexAtom, ctx: AnalysisContext): CharSet { + const memoized = ctx.charSets.get(atom); + if (memoized) return memoized; + const computed = computeCharSet(atom, ctx); + ctx.charSets.set(atom, computed); + return computed; +} + +function computeCharSet(atom: RegexAtom, ctx: AnalysisContext): CharSet { if (atom.kind === "anchor") return new Set(); if (atom.kind === "backref") return "unknown"; if (atom.kind === "group") { @@ -356,12 +390,44 @@ function overlaps(a: CharSet, b: CharSet): boolean { interface AnalysisContext { flags: string; + /** + * Each atom's set is asked for repeatedly — once per neighbour and once per + * alternation pair — and every miss costs a `RegExp` compile plus one probe + * per character. Without this the pair loop is cubic in the pattern length. + */ + charSets: Map; probeChars: readonly string[]; } -/** Atoms that actually consume input — anchors carry no matching cost. */ +/** + * Atoms that actually consume input. Anchors and lookarounds match a position + * rather than text, so they cannot compete with a neighbour for characters — + * counting them makes the standard password rule + * `(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$` read as three chained repetitions when + * it is linear. + */ function consuming(branch: RegexAtom[]): RegexAtom[] { - return branch.filter((atom) => atom.kind !== "anchor"); + return branch.filter((atom) => atom.kind !== "anchor" && !atom.zeroWidth); +} + +/** + * Longest run of input one iteration of this branch can swallow. A body that + * can only ever take a single character cannot hand characters back and forth + * between iterations, which is what separates `(a?)+` and `([A-Z0-9]?){0,16}` + * — both linear — from `(a+)+` and `([a-z]*)*`. + */ +function maxIterationLength(branch: RegexAtom[]): number { + let total = 0; + for (const atom of consuming(branch)) { + if (atom.max === Number.POSITIVE_INFINITY) return Number.POSITIVE_INFINITY; + const body = + atom.kind === "group" && atom.branches?.length + ? Math.max(...atom.branches.map(maxIterationLength)) + : 1; + if (body === Number.POSITIVE_INFINITY) return Number.POSITIVE_INFINITY; + total += atom.max * body; + } + return total; } function describe(atom: RegexAtom): string { @@ -378,6 +444,31 @@ function describe(atom: RegexAtom): string { return `${atom.source}${quantifier}`; } +/** + * Whether two fixed-length branches match exactly the same text, position by + * position. `(ab|ab)+` offers the engine an indistinguishable choice on every + * iteration, which is the same exponential fan-out as `(a|a)+` without being + * two single atoms. `(cat|car)+` differs in its last position and is fine. + */ +function sameLanguage( + a: RegexAtom[], + b: RegexAtom[], + ctx: AnalysisContext, +): boolean { + const left = consuming(a); + const right = consuming(b); + if (left.length === 0 || left.length !== right.length) return false; + return left.every((atom, index) => { + const other = right[index]; + if (atom.min !== other.min || atom.max !== other.max) return false; + const setA = charSetOf(atom, ctx); + const setB = charSetOf(other, ctx); + // Two atoms the probe alphabet cannot describe are not provably different. + if (setA === "unknown" || setB === "unknown") return true; + return setA.size === setB.size && [...setA].every((ch) => setB.has(ch)); + }); +} + function analyzeRepeatedGroup( atom: RegexAtom, ctx: AnalysisContext, @@ -387,6 +478,9 @@ function analyzeRepeatedGroup( for (const branch of branches) { const atoms = consuming(branch); if (atoms.length === 0) continue; + // One character per iteration leaves nothing for the iterations to argue + // over, so none of the splitting rules below can apply. + if (maxIterationLength(branch) < 2) continue; // An inner part that may match nothing lets the outer repetition split the // same input in exponentially many ways. @@ -427,8 +521,8 @@ function analyzeRepeatedGroup( // Alternatives inside a repetition that can both claim the same text. Equal // fixed-length alternatives that merely share a first character (`cat|car`) - // are unambiguous, so length has to differ or one side has to be able to - // stretch before this counts. + // are unambiguous, so length has to differ, one side has to be able to + // stretch, or the two have to match the same text before this counts. for (let i = 0; i < branches.length; i += 1) { for (let j = i + 1; j < branches.length; j += 1) { const a = branches[i]; @@ -438,7 +532,8 @@ function analyzeRepeatedGroup( minLength(a) !== minLength(b) || a.some(isVariableLength) || b.some(isVariableLength) || - (consuming(a).length === 1 && consuming(b).length === 1); + (consuming(a).length === 1 && consuming(b).length === 1) || + sameLanguage(a, b, ctx); if (ambiguous) { return `repeated group \`${describe(atom)}\` has alternatives that can match the same text in more than one way`; } @@ -483,7 +578,10 @@ function walk(branches: RegexAtom[][], ctx: AnalysisContext): string | null { if (chained) return chained; for (const atom of branch) { if (atom.kind !== "group") continue; - if (isUnbounded(atom)) { + // Any group that can iterate more than once re-splits the same input + // across its iterations, and a finite bound does not remove that + // fan-out: `^(a+){10}$` never returns on a 41-character non-match. + if (atom.max > 1) { const reason = analyzeRepeatedGroup(atom, ctx); if (reason) return reason; } @@ -507,6 +605,16 @@ export function analyzeRegexSource( source: string, flags = "", ): RegexSafetyVerdict { + // The analysis itself is super-linear in the pattern length — every pair of + // alternatives is compared — so it needs the same cap it exists to enforce. + // Callers that reach this directly rather than through `compileUserRegex` + // would otherwise trade a slow match for a slow verdict. + if (source.length > MAX_USER_REGEX_LENGTH) { + return { + safe: false, + reason: `pattern is ${source.length} characters; the limit is ${MAX_USER_REGEX_LENGTH}`, + }; + } const state: ParseState = { source, index: 0, depth: 0, bailed: false }; const branches = parseAlternation(state); if (state.bailed || state.index < source.length) { @@ -516,12 +624,16 @@ export function analyzeRegexSource( "pattern uses constructs this validator cannot analyze for catastrophic backtracking", }; } - // Only case folding changes which characters two atoms share; the rest affect - // anchoring or iteration, and `y`/`g` would break the single-character probes. - const probeFlags = flags.includes("i") ? "i" : ""; + // `i` with `u` folds `ſ` onto `s`, and `s` lets `.` cover a newline an + // alternative also matches — both turn a disjoint reading into an ambiguous + // one, so the probes have to run under the caller's flags. + const probeFlags = MATCHING_FLAGS.filter((flag) => flags.includes(flag)).join( + "", + ); const reason = walk(branches, { flags: probeFlags, probeChars: collectProbeChars(source), + charSets: new Map(), }); return reason ? { safe: false, reason } : { safe: true }; } diff --git a/templates/forms/app/pages/FormFillPage.tsx b/templates/forms/app/pages/FormFillPage.tsx index 90592ec9042..e9d7dbbcc37 100644 --- a/templates/forms/app/pages/FormFillPage.tsx +++ b/templates/forms/app/pages/FormFillPage.tsx @@ -167,7 +167,10 @@ export function FormFillPage() { `${field.label} must be at most ${field.validation.max}` ); } - if (field.validation.pattern && typeof val === "string") { + // An empty value is the required check's business. The submit handler + // has always skipped pattern checks for one, so running it here only + // blocks a submission the server would accept. + if (field.validation.pattern && typeof val === "string" && val !== "") { const result = testUserRegex(field.validation.pattern, val); if (result.status === "unevaluated") { return t("publicForm.uncheckablePattern", { label: field.label }); diff --git a/templates/forms/server/handlers/submissions.ts b/templates/forms/server/handlers/submissions.ts index deede5df61f..5b00e7213bc 100644 --- a/templates/forms/server/handlers/submissions.ts +++ b/templates/forms/server/handlers/submissions.ts @@ -1108,10 +1108,13 @@ export const submitForm = defineEventHandler(async (event: H3Event) => { // Parse form fields and build whitelist of valid field IDs. Published forms // must pass the same structural checks as forms at write time because the // public route is also reachable for legacy rows and direct HTTP clients. + // Pattern safety is the one exception: a legacy row carrying an unsafe + // pattern gets the field-level reason from validateSubmissionField below, + // which names the field, rather than a blanket 500 that names nothing. let fields: FormField[]; try { fields = JSON.parse(form.fields); - assertValidFields(fields); + assertValidFields(fields, { patternSafety: false }); } catch { setResponseStatus(event, 500); return { error: "Form configuration is invalid" }; diff --git a/templates/forms/server/handlers/uploads.ts b/templates/forms/server/handlers/uploads.ts index f427318ebb8..34400e6fd76 100644 --- a/templates/forms/server/handlers/uploads.ts +++ b/templates/forms/server/handlers/uploads.ts @@ -65,7 +65,9 @@ export const uploadFormFile = defineEventHandler(async (event: H3Event) => { try { settings = parseStoredFormSettings(form.settings); fields = JSON.parse(form.fields); - assertValidFields(fields); + // An upload never executes a validation pattern, so an unsafe one stored + // on some other field is no reason to refuse the file. + assertValidFields(fields, { patternSafety: false }); } catch { return invalidFormResponse(event); } diff --git a/templates/forms/server/lib/public-form-ssr.ts b/templates/forms/server/lib/public-form-ssr.ts index bb53b99627a..16c534bcd3f 100644 --- a/templates/forms/server/lib/public-form-ssr.ts +++ b/templates/forms/server/lib/public-form-ssr.ts @@ -728,9 +728,13 @@ function renderFormPage( return (f.validation.message || f.label + " must be at least " + f.validation.min); if (f.validation.max != null && Number(v) > f.validation.max) return (f.validation.message || f.label + " must be at most " + f.validation.max); - if (f.validation.unsafePattern) + // An absent value never reaches a pattern check in the React client or + // the submit handler, so an untouched optional field must not fail here + // just because the owner's stored rule is unrunnable. + var hasValue = typeof v === "string" ? v !== "" : v !== undefined && v !== null; + if (f.validation.unsafePattern && hasValue) return f.label + " has a validation rule that cannot be checked. Ask the form owner to fix it."; - if (f.validation.pattern && typeof v === "string") { + if (f.validation.pattern && typeof v === "string" && hasValue) { if (v.length > ${MAX_USER_REGEX_INPUT_LENGTH}) return f.label + " is too long to check against this form's rule."; if (!new RegExp(f.validation.pattern).test(v)) diff --git a/templates/forms/server/lib/validate-fields.ts b/templates/forms/server/lib/validate-fields.ts index aa9f55db889..4ec62880f0f 100644 --- a/templates/forms/server/lib/validate-fields.ts +++ b/templates/forms/server/lib/validate-fields.ts @@ -97,7 +97,20 @@ export function normalizePersistedFields(fields: unknown): unknown { }); } -export function assertValidFields(fields: unknown): void { +/** + * `patternSafety` is the authoring gate: reject a `validation.pattern` that can + * backtrack catastrophically. Read paths pass `false`. A form saved before the + * gate landed still holds such a pattern, and failing its whole configuration + * would answer a submission with a generic 500 instead of the field-level + * reason `validateSubmissionField` produces — which is the message that tells + * the respondent, and through them the owner, what is actually wrong. Nothing + * executes the pattern on the strength of this check; every execution site + * re-tests it through `testUserRegex`. + */ +export function assertValidFields( + fields: unknown, + { patternSafety = true }: { patternSafety?: boolean } = {}, +): void { if (!Array.isArray(fields)) { throw new Error("fields must be an array"); } @@ -234,12 +247,12 @@ export function assertValidFields(fields: unknown): void { `field #${idx + 1} validation.pattern must be a valid regular expression`, ); } - if (compiled.status === "too-long") { + if (patternSafety && compiled.status === "too-long") { throw new Error( `field #${idx + 1} validation.pattern is too long: ${compiled.message}`, ); } - if (compiled.status === "unsafe") { + if (patternSafety && compiled.status === "unsafe") { throw new Error( `field #${idx + 1} validation.pattern can hang the browser and the server: ${compiled.message}. Rewrite it without overlapping repetition — for example use \`^\\S+(\\s+\\S+)+$\` for "at least two words".`, ); diff --git a/templates/forms/server/lib/validation-pattern-redos.spec.ts b/templates/forms/server/lib/validation-pattern-redos.spec.ts index d622a8907b0..9c6580a76df 100644 --- a/templates/forms/server/lib/validation-pattern-redos.spec.ts +++ b/templates/forms/server/lib/validation-pattern-redos.spec.ts @@ -113,4 +113,30 @@ describe("agent-authored validation patterns", () => { expect(shipped?.pattern).toBe(SAFE_TWO_WORDS); expect(shipped?.unsafePattern).toBeUndefined(); }); + + it("lets a legacy form reach the field-level reason on submit", () => { + // The submit and upload handlers re-validate stored fields. Refusing the + // whole configuration there answers with a generic 500 and replaces the + // message that names the offending field, so the safety gate is scoped to + // the write paths. Nothing executes the pattern on the strength of this — + // validateSubmissionField still refuses to run it. + expect(() => + assertValidFields([fullNameField(REPORTED_PATTERN)], { + patternSafety: false, + }), + ).not.toThrow(); + expect(() => + assertValidFields([fullNameField("^(unclosed")], { + patternSafety: false, + }), + ).toThrow(/valid regular expression/i); + }); + + it("does not fail an optional untouched field over the owner's bad rule", () => { + // The submit handler skips pattern checks for an absent value, so a + // respondent must not be blocked on a field they legitimately left empty. + const optional = { ...fullNameField(REPORTED_PATTERN), required: false }; + expect(validateSubmissionField(optional, "")).toBeNull(); + expect(validateSubmissionField(optional, undefined)).toBeNull(); + }); }); diff --git a/templates/slides/server/lib/slide-content-patch.test.ts b/templates/slides/server/lib/slide-content-patch.test.ts index 25fe5ac550a..e55b0e4fe73 100644 --- a/templates/slides/server/lib/slide-content-patch.test.ts +++ b/templates/slides/server/lib/slide-content-patch.test.ts @@ -300,6 +300,47 @@ describe("SlideContentEditError transport contract", () => { } }); + it("refuses a regex-replace pattern that can backtrack catastrophically", async () => { + // `matchAll` over slide content is unbounded work for a pattern like this, + // and nothing can interrupt it once V8 is inside the match, so the refusal + // has to happen before the first match attempt. + const error = await applySlideContentEdits("

aaaaaaaaaaaaaaaaaaaa!

", [ + { op: "regex-replace", pattern: "^([A-Za-z]+\\s?)+$", replace: "x" }, + ]).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(SlideContentEditError); + expect((error as Error).message).toMatch(/cannot be run safely/i); + }); + + it("judges a regex-replace pattern with the flags it will run under", async () => { + // `(a|A)+` is unambiguous on its own and catastrophic under `i`, so the + // verdict has to see the same flags the RegExp is built with. + const safe = await applySlideContentEdits("

aaa

", [ + { op: "regex-replace", pattern: "(a|A)+", replace: "x" }, + ]); + expect(safe.content).toContain("x"); + + const error = await applySlideContentEdits("

aaa

", [ + { op: "regex-replace", pattern: "(a|A)+", replace: "x", flags: "i" }, + ]).catch((caught: unknown) => caught); + expect(error).toBeInstanceOf(SlideContentEditError); + }); + + it("refuses a pattern too long to analyze rather than stalling on it", async () => { + // regex-replace reaches the analyzer directly, without the length cap + // `compileUserRegex` applies. The analysis is itself super-linear in the + // source length — 800 alternatives took 15 seconds — so the bound lives in + // the analyzer, and this proves the Slides path inherits it. + const pattern = `^(${Array.from({ length: 400 }, (_, i) => `a${i}`).join("|")})+$`; + const started = Date.now(); + const error = await applySlideContentEdits("

a1a2

", [ + { op: "regex-replace", pattern, replace: "x" }, + ]).catch((caught: unknown) => caught); + + expect(error).toBeInstanceOf(SlideContentEditError); + expect(Date.now() - started).toBeLessThan(1000); + }); + it("keeps formatter failures out of the caller-correctable contract", async () => { const error = await applySlideContentEdits( "
", From 50861048144af35c6f558637c8ecc9f88ff7361e Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Tue, 15 Sep 2026 19:57:52 +0000 Subject: [PATCH 5/5] chore: run the formatter over docs main left unformatted Forty mdx files arrived unformatted with the latest main merge and fail fmt:check. Column alignment only. --- .../content/locales/ar-SA/plan-plugin.mdx | 16 ++-- .../content/locales/ar-SA/pr-visual-recap.mdx | 86 ++++++++--------- .../ar-SA/template-content-local-files.mdx | 12 +-- .../content/locales/de-DE/pr-visual-recap.mdx | 82 ++++++++-------- .../de-DE/template-content-local-files.mdx | 6 +- .../content/locales/de-DE/template-plan.mdx | 4 +- .../content/locales/es-ES/plan-plugin.mdx | 10 +- .../content/locales/es-ES/pr-visual-recap.mdx | 76 +++++++-------- .../es-ES/template-content-local-files.mdx | 6 +- .../content/locales/es-ES/template-plan.mdx | 4 +- .../content/locales/fr-FR/plan-plugin.mdx | 10 +- .../content/locales/fr-FR/pr-visual-recap.mdx | 85 ++++++++--------- .../fr-FR/template-content-local-files.mdx | 8 +- .../content/locales/hi-IN/plan-plugin.mdx | 2 +- .../content/locales/hi-IN/pr-visual-recap.mdx | 82 ++++++++-------- .../hi-IN/template-content-local-files.mdx | 12 +-- .../content/locales/hi-IN/template-plan.mdx | 4 +- .../content/locales/ja-JP/plan-plugin.mdx | 11 ++- .../content/locales/ja-JP/pr-visual-recap.mdx | 80 ++++++++-------- .../ja-JP/template-content-local-files.mdx | 12 +-- .../content/locales/ja-JP/template-plan.mdx | 4 +- .../content/locales/ko-KR/plan-plugin.mdx | 6 +- .../content/locales/ko-KR/pr-visual-recap.mdx | 91 +++++++++--------- .../ko-KR/template-content-local-files.mdx | 12 +-- .../content/locales/ko-KR/template-plan.mdx | 4 +- .../content/locales/pt-BR/plan-plugin.mdx | 8 +- .../content/locales/pt-BR/pr-visual-recap.mdx | 82 ++++++++-------- .../pt-BR/template-content-local-files.mdx | 4 +- .../content/locales/pt-BR/template-plan.mdx | 2 +- .../content/locales/zh-CN/plan-plugin.mdx | 12 +-- .../content/locales/zh-CN/pr-visual-recap.mdx | 94 +++++++++---------- .../zh-CN/template-content-local-files.mdx | 10 +- .../content/locales/zh-CN/template-plan.mdx | 8 +- .../content/locales/zh-TW/plan-plugin.mdx | 12 +-- .../content/locales/zh-TW/pr-visual-recap.mdx | 90 +++++++++--------- .../zh-TW/template-content-local-files.mdx | 6 +- .../content/locales/zh-TW/template-plan.mdx | 8 +- .../content/template-plan-automations.mdx | 60 ++++++------ .../docs/content/template-plan-developers.mdx | 20 ++-- .../content/template-plan-review-workflow.mdx | 4 +- 40 files changed, 557 insertions(+), 588 deletions(-) diff --git a/packages/core/docs/content/locales/ar-SA/plan-plugin.mdx b/packages/core/docs/content/locales/ar-SA/plan-plugin.mdx index 3138023114c..2b3de4a5756 100644 --- a/packages/core/docs/content/locales/ar-SA/plan-plugin.mdx +++ b/packages/core/docs/content/locales/ar-SA/plan-plugin.mdx @@ -26,9 +26,7 @@ description: "ثبِّت مهارات Agent-Native Plan (/visual-plan، و/visua
- CLI العام
skills add visual-plan + CLI العام
skills add visual-plan
مكوّن Claude Code الإضافي
- مكوّن Codex الإضافي
codex plugin add + مكوّن Codex الإضافي
codex plugin add
@@ -155,16 +155,16 @@ npx @agent-native/core@latest plan local serve --dir plans/ --kind plan -- افتراضي · مستضافالنشر إلى تطبيق Planموصل MCP → قاعدة بيانات مستضافة → روابط مشاركة، وتعليقات، - وسجل، ولقطات شاشةموصل MCP → قاعدة بيانات مستضافة → روابط مشاركة، وتعليقات، وسجل، + ولقطات شاشة
خصوصية الملفات المحليةكتابة MDX على القرصplan.mdx + canvas.mdx + prototype.mdx → جسر localhost → - تقرأ واجهة Plan المستضافة المصدر المحلي. لا كتابة في قاعدة البيانات حتى + >plan.mdx + canvas.mdx + prototype.mdx → جسر localhost → تقرأ + واجهة Plan المستضافة المصدر المحلي. لا كتابة في قاعدة البيانات حتى publish-visual-plan.
diff --git a/packages/core/docs/content/locales/ar-SA/pr-visual-recap.mdx b/packages/core/docs/content/locales/ar-SA/pr-visual-recap.mdx index 41252f71174..cba2e7162a0 100644 --- a/packages/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/ar-SA/pr-visual-recap.mdx @@ -74,8 +74,8 @@ description: "إجراء GitHub يشغّل مهارة visual-recap في مستو
- بالإضافة إلى فحص Visual Recap إعلامي - — غير حاجز، وغير مطلوب أبدًا. + بالإضافة إلى فحص Visual Recap إعلامي — + غير حاجز، وغير مطلوب أبدًا.
``` @@ -198,11 +198,11 @@ GitHub مُشغِّلًا متصلاً يطابق كل تسمية مهيَّأة اختر وكيل الترميز الذي يشغّل المهارة باستخدام متغير المستودع `VISUAL_RECAP_AGENT`: -| `VISUAL_RECAP_AGENT` | وكيل الترميز | مفتاح API المطلوب | المتغيرات المطلوبة | -| --------------------- | ------------------------------- | ----------------------- | ----------------------------------------------- | -| `claude` _(افتراضي)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code مع مزوّد متوافق | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`، `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | وكيل الترميز | مفتاح API المطلوب | المتغيرات المطلوبة | +| -------------------- | --------------------------------- | ---------------------- | --------------------------------------------- | +| `claude` _(افتراضي)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code مع مزوّد متوافق | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`، `VISUAL_RECAP_MODEL` | إذا لم يكن المتغير مضبوطًا، يستخدم الإجراء `claude`. @@ -255,10 +255,10 @@ GitHub مُشغِّلًا متصلاً يطابق كل تسمية مهيَّأة ### الأسرار للواجهة الخلفية الافتراضية -| السر | الغرض | -| ------------------- | ------------------------------------------------------------------------------------------------------------------ | +| السر | الغرض | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | رمز مميز قابل للإلغاء يُصدَر بواسطة `npx @agent-native/core@latest connect`. يخوّل نشر خطة الملخّص وتحميل لقطة الشاشة. | -| `ANTHROPIC_API_KEY` | مفتاح LLM للواجهة الخلفية الافتراضية Claude Code. | +| `ANTHROPIC_API_KEY` | مفتاح LLM للواجهة الخلفية الافتراضية Claude Code. | **للفرق: استخدم رمز خدمة للمؤسسة.** يرتبط الرمز الشخصي بالشخص الذي أصدره — فإذا غادر المؤسسة أو ألغى رموزه، يبدأ كل مستودع يستخدم ذلك السر بالفشل برمز @@ -292,17 +292,17 @@ npx @agent-native/recap-cli@latest recap setup ### اختياري (فقط إذا غيّرت الإعدادات الافتراضية) -| السر / المتغير | الافتراضي | متى تحتاج إليه | -| ------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `OPENAI_API_KEY` | — | سر. اضبطه مع `VISUAL_RECAP_AGENT=codex` لتشغيل الملخّص باستخدام Codex بدلاً من ذلك. | -| `VISUAL_RECAP_API_KEY` | — | سر. اضبطه مع `VISUAL_RECAP_AGENT=openai-compatible` لـ DeepSeek أو Kimi أو مزوّد آخر متوافق مع OpenAI. | -| `VISUAL_RECAP_AGENT` | `claude` | متغير. يحدد الواجهة الخلفية لوكيل الترميز (`claude` أو `codex` أو `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | مطلوب للواجهة الخلفية المتوافقة | متغير. عنوان HTTP(S) الأساسي للواجهة الخلفية المتوافقة مع OpenAI؛ تُرفَض بيانات الاعتماد. | -| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` لـ Claude؛ مطلوب للواجهة الخلفية المتوافقة | متغير. معرّف نموذج المزوّد للواجهات الخلفية المتوافقة مع OpenAI (مطلوب هناك). بالنسبة لـ Claude، يستخدم تركه دون ضبط الآن `claude-sonnet-5` افتراضيًا — اضبطه لتجاوز ذلك، مثل `claude-haiku-4-5` لأرخص فئة. بالنسبة لـ Codex، يستخدم تركه دون ضبط الإعداد الافتراضي الخاص بـ Codex CLI. | -| `VISUAL_RECAP_REASONING` | افتراضي كل نموذج | متغير. عمق الاستدلال: `none`، أو `minimal`، أو `low`، أو `medium`، أو `high`، أو `xhigh`. ينطبق على الواجهة الخلفية Codex. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | متغير. تصنيفات طلب سحب مفصولة بفواصل. عند ضبطه، تتخطى البوابة حتى يحمل طلب السحب تصنيفًا واحدًا على الأقل من المُدرَجين، مثل `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | متغير. يثبّت إصدار `@agent-native/recap-cli` الذي يثبّته سير العمل — مثلاً `1.5.0`. راجع [تثبيت الإصدار](#version-pinning-copy-variant). | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | سر. فقط عند استضافة تطبيق Plans ذاتيًا على مصدر مختلف. | +| السر / المتغير | الافتراضي | متى تحتاج إليه | +| ------------------------------ | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | سر. اضبطه مع `VISUAL_RECAP_AGENT=codex` لتشغيل الملخّص باستخدام Codex بدلاً من ذلك. | +| `VISUAL_RECAP_API_KEY` | — | سر. اضبطه مع `VISUAL_RECAP_AGENT=openai-compatible` لـ DeepSeek أو Kimi أو مزوّد آخر متوافق مع OpenAI. | +| `VISUAL_RECAP_AGENT` | `claude` | متغير. يحدد الواجهة الخلفية لوكيل الترميز (`claude` أو `codex` أو `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | مطلوب للواجهة الخلفية المتوافقة | متغير. عنوان HTTP(S) الأساسي للواجهة الخلفية المتوافقة مع OpenAI؛ تُرفَض بيانات الاعتماد. | +| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` لـ Claude؛ مطلوب للواجهة الخلفية المتوافقة | متغير. معرّف نموذج المزوّد للواجهات الخلفية المتوافقة مع OpenAI (مطلوب هناك). بالنسبة لـ Claude، يستخدم تركه دون ضبط الآن `claude-sonnet-5` افتراضيًا — اضبطه لتجاوز ذلك، مثل `claude-haiku-4-5` لأرخص فئة. بالنسبة لـ Codex، يستخدم تركه دون ضبط الإعداد الافتراضي الخاص بـ Codex CLI. | +| `VISUAL_RECAP_REASONING` | افتراضي كل نموذج | متغير. عمق الاستدلال: `none`، أو `minimal`، أو `low`، أو `medium`، أو `high`، أو `xhigh`. ينطبق على الواجهة الخلفية Codex. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | متغير. تصنيفات طلب سحب مفصولة بفواصل. عند ضبطه، تتخطى البوابة حتى يحمل طلب السحب تصنيفًا واحدًا على الأقل من المُدرَجين، مثل `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | متغير. يثبّت إصدار `@agent-native/recap-cli` الذي يثبّته سير العمل — مثلاً `1.5.0`. راجع [تثبيت الإصدار](#version-pinning-copy-variant). | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | سر. فقط عند استضافة تطبيق Plans ذاتيًا على مصدر مختلف. | يكتشف سير العمل تلقائيًا كيفية استدعاء CLI المساعد الخاص به (المصدر المحلي داخل هذا المستودع الأحادي (monorepo)، أو `@agent-native/recap-cli` المنشور @@ -426,14 +426,14 @@ OpenAI. ### ما يفعله سير عمل Fork وما لا يفعله -| ما يفعله سير العمل | ما لا يفعله سير العمل | -| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| يسحب (checkout) **المستودع الأساسي** عند **مرجع الفرع الأساسي** — كود موثوق فقط | سحب أو تنفيذ أي كود من Fork | -| يجلب رأس Fork كمرجع بعيد (`git fetch origin pull//head:refs/recap/fork-head`) — جلب الالتزامات آمن | تثبيت حزم من Fork، أو تشغيل نصوص Fork، أو تقييم محتوى Fork ككود | -| يشغّل `git diff base...refs/recap/fork-head` — فرق نصي خالص بين كائنين مجلوبين مسبقًا | استخدام الفرق كأي شيء آخر غير إدخال نصي لنموذج LLM | -| يشغّل مهارة visual-recap وتهيئة الوكيل الخاصتين **بالمستودع الأساسي** | تحميل أي مهارة أو تهيئة من Fork | -| يمرّر الفرق عبر خطوة فحص الأسرار نفسها (fail-closed) المستخدَمة لطلبات السحب الأصلية | تخطي فحص الأسرار | -| يضيف ملاحظة صريحة لتحصين الموجّه في موجّه الوكيل تُصنِّف محتوى الفرق كغير موثوق | منح الوكيل أي أذونات إضافية تتجاوز وكيل الملخّص العادي | +| ما يفعله سير العمل | ما لا يفعله سير العمل | +| ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| يسحب (checkout) **المستودع الأساسي** عند **مرجع الفرع الأساسي** — كود موثوق فقط | سحب أو تنفيذ أي كود من Fork | +| يجلب رأس Fork كمرجع بعيد (`git fetch origin pull//head:refs/recap/fork-head`) — جلب الالتزامات آمن | تثبيت حزم من Fork، أو تشغيل نصوص Fork، أو تقييم محتوى Fork ككود | +| يشغّل `git diff base...refs/recap/fork-head` — فرق نصي خالص بين كائنين مجلوبين مسبقًا | استخدام الفرق كأي شيء آخر غير إدخال نصي لنموذج LLM | +| يشغّل مهارة visual-recap وتهيئة الوكيل الخاصتين **بالمستودع الأساسي** | تحميل أي مهارة أو تهيئة من Fork | +| يمرّر الفرق عبر خطوة فحص الأسرار نفسها (fail-closed) المستخدَمة لطلبات السحب الأصلية | تخطي فحص الأسرار | +| يضيف ملاحظة صريحة لتحصين الموجّه في موجّه الوكيل تُصنِّف محتوى الفرق كغير موثوق | منح الوكيل أي أذونات إضافية تتجاوز وكيل الملخّص العادي | ### لماذا يجب عليك مراجعة الفرق قبل التصنيف @@ -469,14 +469,14 @@ OpenAI. بحيث لا يمكن لطلب سحب أن يعيد كتابة سير العمل أو المهارة أو تهيئة الوكيل التي تحمّلها مهمة الملخّص الموثوقة، ثم يسرّب الأسرار: -| نمط المسار | السبب | -| ------------------------------------------- | ---------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | سير العمل نفسه | -| `**/skills/visual-(recap\|plan\|plans)/**` | مهارة visual-recap التي يتّبعها الوكيل | -| `**/.claude/**` | إعدادات الوكيل التي يحمّلها المُشغِّل | -| `**/CLAUDE.md` | تعليمات الوكيل التي يحمّلها المُشغِّل | -| `**/AGENTS.md` | تعليمات الوكيل التي يحمّلها المُشغِّل | -| `**/.mcp.json` | تهيئة خادم MCP التي يحمّلها المُشغِّل | +| نمط المسار | السبب | +| ------------------------------------------ | -------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | سير العمل نفسه | +| `**/skills/visual-(recap\|plan\|plans)/**` | مهارة visual-recap التي يتّبعها الوكيل | +| `**/.claude/**` | إعدادات الوكيل التي يحمّلها المُشغِّل | +| `**/CLAUDE.md` | تعليمات الوكيل التي يحمّلها المُشغِّل | +| `**/AGENTS.md` | تعليمات الوكيل التي يحمّلها المُشغِّل | +| `**/.mcp.json` | تهيئة خادم MCP التي يحمّلها المُشغِّل | في المستودع الأحادي `BuilderIO/agent-native`، يشغّل سير العمل CLI الخاص بالملخّص من مصدر الفرع الأساسي الموثوق بدلاً من مصدر رأس طلب السحب. يبقي هذا @@ -593,12 +593,12 @@ EXAMPLE_API_KEY=placeholder-value `uses:`. يلتقط كل مستدعٍ أحدث منطق تلقائيًا عند تشغيل سير العمل، دون الحاجة إلى تحديث محلي. -| | النسخ (افتراضي) | القابل لإعادة الاستخدام | -| ---------------------------------------- | ------------------------------- | -------------------------------------- | -| حجم سير العمل في مستودعك | نحو 360 سطرًا | نحو 20 سطرًا | -| يلتقط الإصلاحات تلقائيًا | لا — أعِد تشغيل `recap setup` | نعم | -| العزل التام / إمكانية التدقيق الكامل | نعم | لا | -| قابل للتثبيت على إصدار محدد | فقط بالتحرير محليًا | نعم — اضبط `@v1.2.3` في `uses:` | +| | النسخ (افتراضي) | القابل لإعادة الاستخدام | +| ------------------------------------ | ----------------------------- | ------------------------------- | +| حجم سير العمل في مستودعك | نحو 360 سطرًا | نحو 20 سطرًا | +| يلتقط الإصلاحات تلقائيًا | لا — أعِد تشغيل `recap setup` | نعم | +| العزل التام / إمكانية التدقيق الكامل | نعم | لا | +| قابل للتثبيت على إصدار محدد | فقط بالتحرير محليًا | نعم — اضبط `@v1.2.3` في `uses:` | ### مقتطف المستدعي diff --git a/packages/core/docs/content/locales/ar-SA/template-content-local-files.mdx b/packages/core/docs/content/locales/ar-SA/template-content-local-files.mdx index e8bf8ea3cb0..870135ef231 100644 --- a/packages/core/docs/content/locales/ar-SA/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/ar-SA/template-content-local-files.mdx @@ -22,9 +22,7 @@ description: "اربط مجلدات Markdown وMDX بمساحات Content الم
- مساحة موجودة
شخصية أو مؤسسة + مساحة موجودة
شخصية أو مؤسسة
مساحة جديدة مدعومة بمجلد
diff --git a/packages/core/docs/content/locales/de-DE/pr-visual-recap.mdx b/packages/core/docs/content/locales/de-DE/pr-visual-recap.mdx index b1b02df22e0..19b26d9a80f 100644 --- a/packages/core/docs/content/locales/de-DE/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/de-DE/pr-visual-recap.mdx @@ -235,11 +235,11 @@ einen dauerhaften Runner um. Wählen Sie mit der Repository-Variable `VISUAL_RECAP_AGENT`, welcher Coding-Agent den Skill ausführt: -| `VISUAL_RECAP_AGENT` | Coding-Agent | Erforderlicher API-Schlüssel | Erforderliche Variablen | -| --------------------- | --------------------------------------- | ----------------------- | --------------------------------------------- | -| `claude` _(Standard)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + kompatibler Anbieter | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | Coding-Agent | Erforderlicher API-Schlüssel | Erforderliche Variablen | +| --------------------- | ---------------------------------------- | ---------------------------- | --------------------------------------------- | +| `claude` _(Standard)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + kompatibler Anbieter | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | Wenn die Variable nicht gesetzt ist, verwendet die Action `claude`. @@ -344,10 +344,10 @@ Ihres Repositorys. ### Secrets für das Standard-Backend -| Secret | Zweck | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Secret | Zweck | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | Widerrufbares Token, geprägt von `npx @agent-native/core@latest connect`. Autorisiert das Veröffentlichen des Recap-Plans und den Screenshot-Upload. | -| `ANTHROPIC_API_KEY` | Der LLM-Schlüssel für das Standard-Backend Claude Code. | +| `ANTHROPIC_API_KEY` | Der LLM-Schlüssel für das Standard-Backend Claude Code. | **Teams: verwenden Sie ein Org-Service-Token.** Ein persönliches Token ist an die Person gebunden, die es geprägt hat — wenn sie die Org @@ -388,17 +388,17 @@ echtes Token. ### Optional (nur wenn Sie Standardwerte ändern) -| Secret / Variable | Standard | Wann Sie es brauchen | -| ------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret. Zusammen mit `VISUAL_RECAP_AGENT=codex` setzen, um den Recap stattdessen mit Codex auszuführen. | -| `VISUAL_RECAP_API_KEY` | — | Secret. Mit `VISUAL_RECAP_AGENT=openai-compatible` für DeepSeek, Kimi oder einen anderen OpenAI-kompatiblen Anbieter setzen. | -| `VISUAL_RECAP_AGENT` | `claude` | Variable. Wählt das Coding-Agent-Backend (`claude`, `codex` oder `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | erforderlich für kompatibles Backend | Variable. HTTP(S)-Basis-URL für das OpenAI-kompatible Backend; Zugangsdaten werden abgelehnt. | -| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` für Claude; erforderlich für kompatibles Backend | Variable. Anbieter-Modell-ID für OpenAI-kompatible Backends (dort erforderlich). Für Claude verwendet das Nichtsetzen jetzt standardmäßig `claude-sonnet-5` — setzen Sie es zum Überschreiben, z. B. `claude-haiku-4-5` für die günstigste Stufe. Für Codex verwendet das Nichtsetzen das eigene Standardmodell der Codex-CLI. | -| `VISUAL_RECAP_REASONING` | jeweiliger Modellstandard | Variable. Reasoning-Tiefe: `none`, `minimal`, `low`, `medium`, `high` oder `xhigh`. Gilt für das Codex-Backend. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Kommagetrennte PR-Labels. Wenn gesetzt, überspringt das Gate, bis der PR mindestens ein gelistetes Label hat, zum Beispiel `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | Variable. Fixiert die vom Workflow installierte Version von `@agent-native/recap-cli` — z. B. `1.5.0`. Siehe [Versionsfixierung](#version-pinning-copy-variant). | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Nur wenn Sie die Plans-App unter einem anderen Ursprung selbst hosten. | +| Secret / Variable | Standard | Wann Sie es brauchen | +| ------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `OPENAI_API_KEY` | — | Secret. Zusammen mit `VISUAL_RECAP_AGENT=codex` setzen, um den Recap stattdessen mit Codex auszuführen. | +| `VISUAL_RECAP_API_KEY` | — | Secret. Mit `VISUAL_RECAP_AGENT=openai-compatible` für DeepSeek, Kimi oder einen anderen OpenAI-kompatiblen Anbieter setzen. | +| `VISUAL_RECAP_AGENT` | `claude` | Variable. Wählt das Coding-Agent-Backend (`claude`, `codex` oder `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | erforderlich für kompatibles Backend | Variable. HTTP(S)-Basis-URL für das OpenAI-kompatible Backend; Zugangsdaten werden abgelehnt. | +| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` für Claude; erforderlich für kompatibles Backend | Variable. Anbieter-Modell-ID für OpenAI-kompatible Backends (dort erforderlich). Für Claude verwendet das Nichtsetzen jetzt standardmäßig `claude-sonnet-5` — setzen Sie es zum Überschreiben, z. B. `claude-haiku-4-5` für die günstigste Stufe. Für Codex verwendet das Nichtsetzen das eigene Standardmodell der Codex-CLI. | +| `VISUAL_RECAP_REASONING` | jeweiliger Modellstandard | Variable. Reasoning-Tiefe: `none`, `minimal`, `low`, `medium`, `high` oder `xhigh`. Gilt für das Codex-Backend. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Kommagetrennte PR-Labels. Wenn gesetzt, überspringt das Gate, bis der PR mindestens ein gelistetes Label hat, zum Beispiel `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | Variable. Fixiert die vom Workflow installierte Version von `@agent-native/recap-cli` — z. B. `1.5.0`. Siehe [Versionsfixierung](#version-pinning-copy-variant). | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Nur wenn Sie die Plans-App unter einem anderen Ursprung selbst hosten. | Der Workflow erkennt automatisch, wie er seine Helfer-CLI aufruft (lokale Quelle innerhalb dieses Monorepos, das veröffentlichte @@ -540,14 +540,14 @@ wird. ### Was der Fork-Workflow tut und was NICHT -| Der Workflow TUT | Der Workflow tut NICHT | -| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | -| Das **Base-Repository** am **Base-Branch-Ref** auschecken — nur vertrauenswürdiger Code | Irgendwelchen Code aus dem Fork auschecken oder ausführen | -| Den Fork-Head als Remote-Ref abrufen (`git fetch origin pull//head:refs/recap/fork-head`) — Commits abzurufen ist sicher | Pakete aus dem Fork installieren, Fork-Skripte ausführen oder Fork-Inhalte als Code auswerten | -| `git diff base...refs/recap/fork-head` ausführen — reiner Text-Diff zweier bereits abgerufener Objekte | Den Diff als irgendetwas anderes als Text-Eingabe für das LLM verwenden | -| Den `visual-recap`-Skill und die Agentenkonfiguration des **Base-Repos** ausführen | Irgendeinen Skill oder eine Konfiguration aus dem Fork laden | -| Den Diff durch denselben Secret-Scan-Schritt (fail-closed) wie First-Party-PRs leiten | Den Secret-Scan überspringen | -| Einen expliziten Prompt-Hardening-Hinweis zum Agenten-Prompt hinzufügen, der Diff-Inhalt als nicht vertrauenswürdig markiert | Dem Agenten zusätzliche Berechtigungen über den normalen Recap-Agenten hinaus gewähren | +| Der Workflow TUT | Der Workflow tut NICHT | +| ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| Das **Base-Repository** am **Base-Branch-Ref** auschecken — nur vertrauenswürdiger Code | Irgendwelchen Code aus dem Fork auschecken oder ausführen | +| Den Fork-Head als Remote-Ref abrufen (`git fetch origin pull//head:refs/recap/fork-head`) — Commits abzurufen ist sicher | Pakete aus dem Fork installieren, Fork-Skripte ausführen oder Fork-Inhalte als Code auswerten | +| `git diff base...refs/recap/fork-head` ausführen — reiner Text-Diff zweier bereits abgerufener Objekte | Den Diff als irgendetwas anderes als Text-Eingabe für das LLM verwenden | +| Den `visual-recap`-Skill und die Agentenkonfiguration des **Base-Repos** ausführen | Irgendeinen Skill oder eine Konfiguration aus dem Fork laden | +| Den Diff durch denselben Secret-Scan-Schritt (fail-closed) wie First-Party-PRs leiten | Den Secret-Scan überspringen | +| Einen expliziten Prompt-Hardening-Hinweis zum Agenten-Prompt hinzufügen, der Diff-Inhalt als nicht vertrauenswürdig markiert | Dem Agenten zusätzliche Berechtigungen über den normalen Recap-Agenten hinaus gewähren | ### Warum Sie den Diff vor dem Labeln überprüfen müssen @@ -592,14 +592,14 @@ einen der folgenden Pfade berührt, sodass ein PR nie den Workflow, den Skill oder die Agentenkonfiguration umschreiben kann, die der vertrauenswürdige Recap-Job lädt, und dabei Secrets exfiltrieren kann: -| Pfadmuster | Grund | -| -------------------------------------------- | -------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | Der Workflow selbst | -| `**/skills/visual-(recap\|plan\|plans)/**` | Der visual-recap-Skill, dem der Agent folgt | -| `**/.claude/**` | Agenteneinstellungen, die der Runner lädt | -| `**/CLAUDE.md` | Agentenanweisungen, die der Runner lädt | -| `**/AGENTS.md` | Agentenanweisungen, die der Runner lädt | -| `**/.mcp.json` | MCP-Server-Konfiguration, die der Runner lädt | +| Pfadmuster | Grund | +| ------------------------------------------ | --------------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | Der Workflow selbst | +| `**/skills/visual-(recap\|plan\|plans)/**` | Der visual-recap-Skill, dem der Agent folgt | +| `**/.claude/**` | Agenteneinstellungen, die der Runner lädt | +| `**/CLAUDE.md` | Agentenanweisungen, die der Runner lädt | +| `**/AGENTS.md` | Agentenanweisungen, die der Runner lädt | +| `**/.mcp.json` | MCP-Server-Konfiguration, die der Runner lädt | Im Monorepo `BuilderIO/agent-native` führt der Workflow die Recap-CLI aus vertrauenswürdiger Base-Branch-Quelle statt aus @@ -739,12 +739,12 @@ Die **wiederverwendbare** Option schreibt stattdessen einen dünnen Jeder Caller übernimmt automatisch die neueste Logik, wenn der Workflow läuft, ohne lokale Aktualisierung. -| | Copy (Standard) | Wiederverwendbar | -| ------------------------------------ | ----------------------------- | --------------------------------- | -| Workflow-Größe in Ihrem Repo | ~360 Zeilen | ~20 Zeilen | -| Übernimmt Fixes automatisch | Nein — `recap setup` erneut ausführen | Ja | -| Air-Gap / vollständige Prüfbarkeit | Ja | Nein | -| Auf bestimmte Version fixierbar | Nur durch lokales Bearbeiten | Ja — `@v1.2.3` in `uses:` setzen | +| | Copy (Standard) | Wiederverwendbar | +| ---------------------------------- | ------------------------------------- | -------------------------------- | +| Workflow-Größe in Ihrem Repo | ~360 Zeilen | ~20 Zeilen | +| Übernimmt Fixes automatisch | Nein — `recap setup` erneut ausführen | Ja | +| Air-Gap / vollständige Prüfbarkeit | Ja | Nein | +| Auf bestimmte Version fixierbar | Nur durch lokales Bearbeiten | Ja — `@v1.2.3` in `uses:` setzen | ### Caller-Snippet diff --git a/packages/core/docs/content/locales/de-DE/template-content-local-files.mdx b/packages/core/docs/content/locales/de-DE/template-content-local-files.mdx index 416f8d566e4..082107596b4 100644 --- a/packages/core/docs/content/locales/de-DE/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/de-DE/template-content-local-files.mdx @@ -93,11 +93,11 @@ Bereich mit eigener Files-Sammlung. ### Eine Wahrheitsrichtlinie wählen -| Richtlinie | Verhalten | -| ------------------------ | ------------------------------------------------------------------ | +| Richtlinie | Verhalten | +| ------------------------ | ----------------------------------------------------------------------------- | | `database_primary` | Content ist maßgeblich; Änderungen im Ordner werden vor der Übernahme geprüft | | `source_primary` | Der Ordner ist maßgeblich, wenn keine gleichzeitige Content-Änderung vorliegt | -| `reviewed_bidirectional` | Änderungen in beide Richtungen erfordern bei Konflikten eine Prüfung | +| `reviewed_bidirectional` | Änderungen in beide Richtungen erfordern bei Konflikten eine Prüfung | diff --git a/packages/core/docs/content/locales/de-DE/template-plan.mdx b/packages/core/docs/content/locales/de-DE/template-plan.mdx index 3680c27e8a4..84381fc720b 100644 --- a/packages/core/docs/content/locales/de-DE/template-plan.mdx +++ b/packages/core/docs/content/locales/de-DE/template-plan.mdx @@ -60,7 +60,9 @@ zurückgeben, egal welchen Befehl Sie verwenden.
- Coding-Agent
Feedback zurückgegeben + Coding-Agent
Feedback zurückgegeben
``` diff --git a/packages/core/docs/content/locales/es-ES/plan-plugin.mdx b/packages/core/docs/content/locales/es-ES/plan-plugin.mdx index 6e46273cdfe..26134c2f6d7 100644 --- a/packages/core/docs/content/locales/es-ES/plan-plugin.mdx +++ b/packages/core/docs/content/locales/es-ES/plan-plugin.mdx @@ -149,17 +149,17 @@ disponibles hasta que publiques explícitamente más adelante. Predeterminado · alojadoPublicar en la app Planconector MCP → base de datos alojada → enlaces para - compartir, comentarios, historial, capturas de pantallaconector MCP → base de datos alojada → enlaces para compartir, + comentarios, historial, capturas de pantalla
Privacidad de archivos localesEscribir MDX en discoplan.mdx + canvas.mdx + prototype.mdx → puente localhost → - la interfaz de Plan alojada lee la fuente local. Sin escrituras en la - base de datos hasta publish-visual-plan.plan.mdx + canvas.mdx + prototype.mdx → puente localhost → la + interfaz de Plan alojada lee la fuente local. Sin escrituras en la base de + datos hasta publish-visual-plan.
diff --git a/packages/core/docs/content/locales/es-ES/pr-visual-recap.mdx b/packages/core/docs/content/locales/es-ES/pr-visual-recap.mdx index f80dce5133b..eb6238e4d69 100644 --- a/packages/core/docs/content/locales/es-ES/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/es-ES/pr-visual-recap.mdx @@ -191,11 +191,11 @@ dirijas contribuciones no confiables a un ejecutor persistente. Elige qué agente de codificación ejecuta la skill con la variable de repositorio `VISUAL_RECAP_AGENT`: -| `VISUAL_RECAP_AGENT` | Agente de codificación | Clave de API necesaria | Variables necesarias | -| --------------------- | ---------------------------------------- | ----------------------- | ---------------------------------------------- | -| `claude` _(predeterminado)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + proveedor compatible | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | Agente de codificación | Clave de API necesaria | Variables necesarias | +| --------------------------- | ---------------------------------------- | ---------------------- | --------------------------------------------- | +| `claude` _(predeterminado)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + proveedor compatible | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | Si la variable no está definida, la action usa `claude`. @@ -237,10 +237,10 @@ Configura estos valores en **Settings → Secrets and variables → Actions** de ### Secrets para el backend predeterminado -| Secret | Propósito | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Secret | Propósito | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `PLAN_RECAP_TOKEN` | Token revocable generado por `npx @agent-native/core@latest connect`. Autoriza publicar el plan de resumen y subir la captura de pantalla. | -| `ANTHROPIC_API_KEY` | La clave del LLM para el backend predeterminado de Claude Code. | +| `ANTHROPIC_API_KEY` | La clave del LLM para el backend predeterminado de Claude Code. | **Equipos: usa un token de servicio de la organización.** Un token personal está vinculado a la persona que lo generó: si abandona la organización o revoca sus tokens, todos los @@ -275,17 +275,17 @@ real. ### Opcional (solo si cambias los valores predeterminados) -| Secret / variable | Valor predeterminado | Cuándo lo necesitas | -| ------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret. Se establece junto con `VISUAL_RECAP_AGENT=codex` para ejecutar el resumen con Codex en su lugar. | -| `VISUAL_RECAP_API_KEY` | — | Secret. Se establece con `VISUAL_RECAP_AGENT=openai-compatible` para DeepSeek, Kimi u otro proveedor compatible con OpenAI. | -| `VISUAL_RECAP_AGENT` | `claude` | Variable. Selecciona el backend del agente de codificación (`claude`, `codex` u `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | obligatoria para el backend compatible | Variable. URL base HTTP(S) para el backend compatible con OpenAI; se rechazan las credenciales. | +| Secret / variable | Valor predeterminado | Cuándo lo necesitas | +| ------------------------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | Secret. Se establece junto con `VISUAL_RECAP_AGENT=codex` para ejecutar el resumen con Codex en su lugar. | +| `VISUAL_RECAP_API_KEY` | — | Secret. Se establece con `VISUAL_RECAP_AGENT=openai-compatible` para DeepSeek, Kimi u otro proveedor compatible con OpenAI. | +| `VISUAL_RECAP_AGENT` | `claude` | Variable. Selecciona el backend del agente de codificación (`claude`, `codex` u `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | obligatoria para el backend compatible | Variable. URL base HTTP(S) para el backend compatible con OpenAI; se rechazan las credenciales. | | `VISUAL_RECAP_MODEL` | `claude-sonnet-5` para Claude; obligatoria para el backend compatible | Variable. Id de modelo del proveedor para backends compatibles con OpenAI (obligatoria en ese caso). Para Claude, si no se define ahora usa por defecto `claude-sonnet-5`; establécela para anularlo, por ejemplo `claude-haiku-4-5` para el nivel más económico. Para Codex, si no se define usa el propio valor predeterminado de la CLI de Codex. | -| `VISUAL_RECAP_REASONING` | el valor predeterminado de cada modelo | Variable. Profundidad de razonamiento: `none`, `minimal`, `low`, `medium`, `high` o `xhigh`. Se aplica al backend Codex. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Etiquetas de PR separadas por comas. Cuando se establece, la barrera se omite hasta que el PR tenga al menos una de las etiquetas indicadas, por ejemplo `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | Variable. Fija la versión de `@agent-native/recap-cli` que instala el flujo de trabajo — por ejemplo `1.5.0`. Consulta [Fijar la versión](#version-pinning-copy-variant). | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Solo cuando autoalojas la app Plans en un origen diferente. | +| `VISUAL_RECAP_REASONING` | el valor predeterminado de cada modelo | Variable. Profundidad de razonamiento: `none`, `minimal`, `low`, `medium`, `high` o `xhigh`. Se aplica al backend Codex. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Etiquetas de PR separadas por comas. Cuando se establece, la barrera se omite hasta que el PR tenga al menos una de las etiquetas indicadas, por ejemplo `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | Variable. Fija la versión de `@agent-native/recap-cli` que instala el flujo de trabajo — por ejemplo `1.5.0`. Consulta [Fijar la versión](#version-pinning-copy-variant). | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Solo cuando autoalojas la app Plans en un origen diferente. | El flujo de trabajo detecta automáticamente cómo invocar su CLI auxiliar (fuente local dentro de este monorepo, el paquete `@agent-native/recap-cli` publicado en cualquier otro sitio), así @@ -404,14 +404,14 @@ Para instalarlo, copia el archivo desde [BuilderIO/agent-native](https://github. ### Qué hace y qué NO hace el flujo de trabajo de fork -| El flujo de trabajo SÍ hace | El flujo de trabajo NO hace | -| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | -| Descarga el **repositorio base** en la ref de la **rama base** — solo código de confianza | Descargar o ejecutar ningún código del fork | +| El flujo de trabajo SÍ hace | El flujo de trabajo NO hace | +| ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| Descarga el **repositorio base** en la ref de la **rama base** — solo código de confianza | Descargar o ejecutar ningún código del fork | | Obtiene el head del fork como ref remota (`git fetch origin pull//head:refs/recap/fork-head`) — obtener commits es seguro | Instalar paquetes del fork, ejecutar scripts del fork o evaluar contenido del fork como código | -| Ejecuta `git diff base...refs/recap/fork-head` — un diff de texto puro entre dos objetos ya obtenidos | Usar el diff como algo distinto de una entrada de texto para el LLM | -| Ejecuta la skill `visual-recap` y la configuración de agente **del repositorio base** | Cargar ninguna skill ni configuración del fork | -| Pasa el diff por el mismo paso de escaneo de secrets (fail-closed) que los PR propios | Omitir el escaneo de secrets | -| Añade una nota explícita de refuerzo del prompt que marca el contenido del diff como no confiable | Conceder al agente ningún permiso adicional más allá del agente de resumen normal | +| Ejecuta `git diff base...refs/recap/fork-head` — un diff de texto puro entre dos objetos ya obtenidos | Usar el diff como algo distinto de una entrada de texto para el LLM | +| Ejecuta la skill `visual-recap` y la configuración de agente **del repositorio base** | Cargar ninguna skill ni configuración del fork | +| Pasa el diff por el mismo paso de escaneo de secrets (fail-closed) que los PR propios | Omitir el escaneo de secrets | +| Añade una nota explícita de refuerzo del prompt que marca el contenido del diff como no confiable | Conceder al agente ningún permiso adicional más allá del agente de resumen normal | ### Por qué debes revisar el diff antes de etiquetar @@ -448,13 +448,13 @@ El paso `gate` omite el resumen por completo cuando un PR toca cualquiera de las rutas, de modo que un PR nunca puede reescribir el flujo de trabajo, la skill o la configuración de agente que carga el job de resumen de confianza, ni exfiltrar secrets: -| Patrón de ruta | Motivo | -| ------------------------------------------ | ------------------------------------------ | -| `.github/workflows/pr-visual-recap.yml` | El propio flujo de trabajo | -| `**/skills/visual-(recap\|plan\|plans)/**` | La skill `visual-recap` que sigue el agente | -| `**/.claude/**` | Configuración del agente que carga el ejecutor | -| `**/CLAUDE.md` | Instrucciones del agente que carga el ejecutor | -| `**/AGENTS.md` | Instrucciones del agente que carga el ejecutor | +| Patrón de ruta | Motivo | +| ------------------------------------------ | ---------------------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | El propio flujo de trabajo | +| `**/skills/visual-(recap\|plan\|plans)/**` | La skill `visual-recap` que sigue el agente | +| `**/.claude/**` | Configuración del agente que carga el ejecutor | +| `**/CLAUDE.md` | Instrucciones del agente que carga el ejecutor | +| `**/AGENTS.md` | Instrucciones del agente que carga el ejecutor | | `**/.mcp.json` | Configuración del servidor MCP que carga el ejecutor | En el monorepo `BuilderIO/agent-native`, el flujo de trabajo ejecuta la CLI de resumen desde @@ -555,12 +555,12 @@ El instalador predeterminado copia el YAML completo del flujo de trabajo (~360 l La opción **reutilizable** escribe en su lugar un llamador ligero de ~20 líneas. Delega en `BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml` mediante `uses:`. Cada llamador recoge automáticamente la lógica más reciente cuando se ejecuta el flujo de trabajo, sin necesidad de ninguna actualización local. -| | Copia (predeterminada) | Reutilizable | -| ---------------------------------------- | ---------------------------- | ------------------------------- | -| Tamaño del flujo de trabajo en tu repositorio | ~360 líneas | ~20 líneas | -| Recoge correcciones automáticamente | No — hay que volver a ejecutar `recap setup` | Sí | -| Air-gap / auditabilidad completa | Sí | No | -| Se puede fijar a una versión concreta | Solo editando localmente | Sí — establece `@v1.2.3` en `uses:` | +| | Copia (predeterminada) | Reutilizable | +| --------------------------------------------- | -------------------------------------------- | ----------------------------------- | +| Tamaño del flujo de trabajo en tu repositorio | ~360 líneas | ~20 líneas | +| Recoge correcciones automáticamente | No — hay que volver a ejecutar `recap setup` | Sí | +| Air-gap / auditabilidad completa | Sí | No | +| Se puede fijar a una versión concreta | Solo editando localmente | Sí — establece `@v1.2.3` en `uses:` | ### Fragmento del llamador diff --git a/packages/core/docs/content/locales/es-ES/template-content-local-files.mdx b/packages/core/docs/content/locales/es-ES/template-content-local-files.mdx index 073e14c6d44..9a79fc36844 100644 --- a/packages/core/docs/content/locales/es-ES/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/es-ES/template-content-local-files.mdx @@ -108,11 +108,11 @@ colección Files. ### Elegir una política de verdad -| Política | Comportamiento | -| ------------------------ | ------------------------------------------------------------------------ | +| Política | Comportamiento | +| ------------------------ | ------------------------------------------------------------------------------------- | | `database_primary` | Content es la fuente autorizada; los cambios en la carpeta se revisan antes de usarse | | `source_primary` | La carpeta es la fuente autorizada cuando no existe una edición simultánea en Content | -| `reviewed_bidirectional` | Los cambios en cualquier dirección requieren revisión ante conflictos | +| `reviewed_bidirectional` | Los cambios en cualquier dirección requieren revisión ante conflictos | diff --git a/packages/core/docs/content/locales/es-ES/template-plan.mdx b/packages/core/docs/content/locales/es-ES/template-plan.mdx index 1780303aaef..f706a2dc76e 100644 --- a/packages/core/docs/content/locales/es-ES/template-plan.mdx +++ b/packages/core/docs/content/locales/es-ES/template-plan.mdx @@ -58,7 +58,9 @@ le devuelves feedback al agente de la misma manera en ambos casos.
- Agente de codificación
feedback devuelto + Agente de codificación
feedback devuelto
``` diff --git a/packages/core/docs/content/locales/fr-FR/plan-plugin.mdx b/packages/core/docs/content/locales/fr-FR/plan-plugin.mdx index 371fd9eab37..16da816ed1b 100644 --- a/packages/core/docs/content/locales/fr-FR/plan-plugin.mdx +++ b/packages/core/docs/content/locales/fr-FR/plan-plugin.mdx @@ -168,17 +168,17 @@ explicitement plus tard. Par défaut · hébergéPublier dans l'application PlanConnecteur MCP → base de données hébergée → liens de - partage, commentaires, historique, captures d'écranConnecteur MCP → base de données hébergée → liens de partage, + commentaires, historique, captures d'écran
Confidentialité fichiers locauxÉcrire du MDX sur le disqueplan.mdx + canvas.mdx + prototype.mdx → pont localhost → - l'UI Plan hébergée lit la source locale. Aucune écriture en base - jusqu'à publish-visual-plan.plan.mdx + canvas.mdx + prototype.mdx → pont localhost → l'UI + Plan hébergée lit la source locale. Aucune écriture en base jusqu'à + publish-visual-plan.
diff --git a/packages/core/docs/content/locales/fr-FR/pr-visual-recap.mdx b/packages/core/docs/content/locales/fr-FR/pr-visual-recap.mdx index 0c0391a4765..183799a2554 100644 --- a/packages/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/fr-FR/pr-visual-recap.mdx @@ -89,7 +89,8 @@ persistant est une aide à la revue, pas un feu vert.
- Plus une vérification informative Visual Recap + Plus une vérification informative + Visual Recap — non bloquante, jamais requise.
``` @@ -229,11 +230,11 @@ contributions non fiables vers un runner persistant. Choisissez quel agent de codage exécute la compétence avec la variable de dépôt `VISUAL_RECAP_AGENT` : -| `VISUAL_RECAP_AGENT` | Agent de codage | Clé API requise | Variables requises | -| --------------------- | ------------------------------------------- | ----------------------- | --------------------------------------------- | -| `claude` _(par défaut)_ | CLI Claude Code | `ANTHROPIC_API_KEY` | — | -| `codex` | CLI OpenAI Codex | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + fournisseur compatible | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | Agent de codage | Clé API requise | Variables requises | +| ----------------------- | ------------------------------------------ | ---------------------- | --------------------------------------------- | +| `claude` _(par défaut)_ | CLI Claude Code | `ANTHROPIC_API_KEY` | — | +| `codex` | CLI OpenAI Codex | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + fournisseur compatible | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | Si la variable n'est pas définie, l'action utilise `claude`. @@ -337,10 +338,10 @@ votre dépôt. ### Secrets pour le backend par défaut -| Secret | Objectif | -| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Secret | Objectif | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | Jeton révocable créé par `npx @agent-native/core@latest connect`. Autorise la publication du plan récapitulatif et le téléversement de la capture d'écran. | -| `ANTHROPIC_API_KEY` | La clé LLM pour le backend Claude Code par défaut. | +| `ANTHROPIC_API_KEY` | La clé LLM pour le backend Claude Code par défaut. | **Équipes : utilisez un jeton de service d'organisation.** Un jeton personnel est lié à la personne qui l'a créé — si elle quitte @@ -382,17 +383,17 @@ jamais de vrai jeton. ### Optionnel (uniquement si vous changez les défauts) -| Secret / variable | Défaut | Quand vous en avez besoin | -| ------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret. À définir avec `VISUAL_RECAP_AGENT=codex` pour exécuter le récapitulatif avec Codex à la place. | -| `VISUAL_RECAP_API_KEY` | — | Secret. À définir avec `VISUAL_RECAP_AGENT=openai-compatible` pour DeepSeek, Kimi, ou un autre fournisseur compatible OpenAI. | -| `VISUAL_RECAP_AGENT` | `claude` | Variable. Sélectionne le backend agent de codage (`claude`, `codex`, ou `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | requise pour le backend compatible | Variable. URL de base HTTP(S) pour le backend compatible OpenAI ; les identifiants sont rejetés. | -| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` pour Claude ; requise pour le backend compatible | Variable. Identifiant de modèle fournisseur pour les backends compatibles OpenAI (requis dans ce cas). Pour Claude, non défini vaut désormais par défaut `claude-sonnet-5` — définissez-la pour la remplacer, par exemple `claude-haiku-4-5` pour le tier le moins cher. Pour Codex, non défini utilise le modèle par défaut propre à la CLI Codex. | -| `VISUAL_RECAP_REASONING` | défaut propre à chaque modèle | Variable. Profondeur de raisonnement : `none`, `minimal`, `low`, `medium`, `high`, ou `xhigh`. S'applique au backend Codex. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Étiquettes de PR séparées par des virgules. Une fois définie, la vérification saute tant que la PR n'a pas au moins une étiquette listée, par exemple `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | Variable. Épingle la version de `@agent-native/recap-cli` que le workflow installe — par exemple `1.5.0`. Voir [Épinglage de version](#version-pinning-copy-variant). | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Uniquement en cas d'auto-hébergement de l'application Plans sur une autre origine. | +| Secret / variable | Défaut | Quand vous en avez besoin | +| ------------------------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | Secret. À définir avec `VISUAL_RECAP_AGENT=codex` pour exécuter le récapitulatif avec Codex à la place. | +| `VISUAL_RECAP_API_KEY` | — | Secret. À définir avec `VISUAL_RECAP_AGENT=openai-compatible` pour DeepSeek, Kimi, ou un autre fournisseur compatible OpenAI. | +| `VISUAL_RECAP_AGENT` | `claude` | Variable. Sélectionne le backend agent de codage (`claude`, `codex`, ou `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | requise pour le backend compatible | Variable. URL de base HTTP(S) pour le backend compatible OpenAI ; les identifiants sont rejetés. | +| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` pour Claude ; requise pour le backend compatible | Variable. Identifiant de modèle fournisseur pour les backends compatibles OpenAI (requis dans ce cas). Pour Claude, non défini vaut désormais par défaut `claude-sonnet-5` — définissez-la pour la remplacer, par exemple `claude-haiku-4-5` pour le tier le moins cher. Pour Codex, non défini utilise le modèle par défaut propre à la CLI Codex. | +| `VISUAL_RECAP_REASONING` | défaut propre à chaque modèle | Variable. Profondeur de raisonnement : `none`, `minimal`, `low`, `medium`, `high`, ou `xhigh`. S'applique au backend Codex. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variable. Étiquettes de PR séparées par des virgules. Une fois définie, la vérification saute tant que la PR n'a pas au moins une étiquette listée, par exemple `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | Variable. Épingle la version de `@agent-native/recap-cli` que le workflow installe — par exemple `1.5.0`. Voir [Épinglage de version](#version-pinning-copy-variant). | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Uniquement en cas d'auto-hébergement de l'application Plans sur une autre origine. | Le workflow détecte automatiquement comment invoquer sa CLI auxiliaire (source locale à l'intérieur de ce monorepo, `@agent-native/recap-cli` @@ -536,14 +537,14 @@ OpenAI. ### Ce que fait et ne fait PAS le workflow de fork -| Le workflow FAIT | Le workflow ne FAIT PAS | -| ----------------------------------------------------------------------------------------------------------------------------------------| ------------------------------------------------------------------------------------------------------------------ | -| Consulter le **dépôt de base** sur la **réf. de branche de base** — code de confiance uniquement | Faire le checkout ou exécuter du code du fork | -| Récupérer la tête du fork comme réf. distante (`git fetch origin pull//head:refs/recap/fork-head`) — récupérer des commits est sûr | Installer des paquets depuis le fork, exécuter des scripts du fork, ou évaluer du contenu du fork comme du code | -| Exécuter `git diff base...refs/recap/fork-head` — un diff texte pur de deux objets déjà récupérés | Utiliser le diff comme autre chose qu'une entrée texte pour le LLM | -| Exécuter la compétence visual-recap et la configuration d'agent du **dépôt de base** | Charger une compétence ou une configuration depuis le fork | -| Faire passer le diff par la même étape de scan de secrets (échec fermé) que les PR internes | Sauter le scan de secrets | -| Ajouter au prompt de l'agent une note explicite de durcissement marquant le contenu du diff comme non fiable | Accorder à l'agent des permissions supplémentaires au-delà de l'agent de récapitulatif normal | +| Le workflow FAIT | Le workflow ne FAIT PAS | +| ------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Consulter le **dépôt de base** sur la **réf. de branche de base** — code de confiance uniquement | Faire le checkout ou exécuter du code du fork | +| Récupérer la tête du fork comme réf. distante (`git fetch origin pull//head:refs/recap/fork-head`) — récupérer des commits est sûr | Installer des paquets depuis le fork, exécuter des scripts du fork, ou évaluer du contenu du fork comme du code | +| Exécuter `git diff base...refs/recap/fork-head` — un diff texte pur de deux objets déjà récupérés | Utiliser le diff comme autre chose qu'une entrée texte pour le LLM | +| Exécuter la compétence visual-recap et la configuration d'agent du **dépôt de base** | Charger une compétence ou une configuration depuis le fork | +| Faire passer le diff par la même étape de scan de secrets (échec fermé) que les PR internes | Sauter le scan de secrets | +| Ajouter au prompt de l'agent une note explicite de durcissement marquant le contenu du diff comme non fiable | Accorder à l'agent des permissions supplémentaires au-delà de l'agent de récapitulatif normal | ### Pourquoi vous devez relire le diff avant d'étiqueter @@ -590,14 +591,14 @@ l'un des chemins suivants, si bien qu'une PR ne peut jamais réécrire le workflow, la compétence, ou la configuration d'agent que le job de récapitulatif de confiance charge, et exfiltrer des secrets : -| Motif de chemin | Raison | -| ------------------------------------------- | --------------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | Le workflow lui-même | -| `**/skills/visual-(recap\|plan\|plans)/**` | La compétence visual-recap que suit l'agent | -| `**/.claude/**` | Paramètres d'agent que charge le runner | -| `**/CLAUDE.md` | Instructions d'agent que charge le runner | -| `**/AGENTS.md` | Instructions d'agent que charge le runner | -| `**/.mcp.json` | Configuration de serveur MCP que charge le runner | +| Motif de chemin | Raison | +| ------------------------------------------ | ------------------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | Le workflow lui-même | +| `**/skills/visual-(recap\|plan\|plans)/**` | La compétence visual-recap que suit l'agent | +| `**/.claude/**` | Paramètres d'agent que charge le runner | +| `**/CLAUDE.md` | Instructions d'agent que charge le runner | +| `**/AGENTS.md` | Instructions d'agent que charge le runner | +| `**/.mcp.json` | Configuration de serveur MCP que charge le runner | Dans le monorepo `BuilderIO/agent-native`, le workflow exécute la CLI de récapitulatif depuis une source de branche de base de confiance plutôt @@ -741,12 +742,12 @@ via `uses:`. Chaque appelant récupère automatiquement la dernière logique à chaque exécution du workflow, sans mise à jour locale nécessaire. -| | Copie (par défaut) | Réutilisable | -| -------------------------------------- | ----------------------------------- | ------------------------------------------ | -| Taille du workflow dans votre dépôt | ~360 lignes | ~20 lignes | -| Récupère les corrections automatiquement | Non — réexécutez `recap setup` | Oui | -| Air-gap / auditabilité complète | Oui | Non | -| Épinglable à une version spécifique | Uniquement en éditant localement | Oui — définissez `@v1.2.3` dans `uses:` | +| | Copie (par défaut) | Réutilisable | +| ---------------------------------------- | -------------------------------- | --------------------------------------- | +| Taille du workflow dans votre dépôt | ~360 lignes | ~20 lignes | +| Récupère les corrections automatiquement | Non — réexécutez `recap setup` | Oui | +| Air-gap / auditabilité complète | Oui | Non | +| Épinglable à une version spécifique | Uniquement en éditant localement | Oui — définissez `@v1.2.3` dans `uses:` | ### Extrait de l'appelant diff --git a/packages/core/docs/content/locales/fr-FR/template-content-local-files.mdx b/packages/core/docs/content/locales/fr-FR/template-content-local-files.mdx index 38e58303909..a286e956db5 100644 --- a/packages/core/docs/content/locales/fr-FR/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/fr-FR/template-content-local-files.mdx @@ -93,11 +93,11 @@ Files. ### Choisir une stratégie de vérité -| Politique | Comportement | -| ------------------------ | ------------------------------------------------------------------------ | +| Politique | Comportement | +| ------------------------ | ------------------------------------------------------------------------------------------- | | `database_primary` | Content fait autorité ; les modifications du dossier sont examinées avant d'être appliquées | -| `source_primary` | Le dossier fait autorité tant qu'aucune modification Content concurrente n'existe | -| `reviewed_bidirectional` | Les modifications dans les deux sens nécessitent un examen en cas de conflit | +| `source_primary` | Le dossier fait autorité tant qu'aucune modification Content concurrente n'existe | +| `reviewed_bidirectional` | Les modifications dans les deux sens nécessitent un examen en cas de conflit | diff --git a/packages/core/docs/content/locales/hi-IN/plan-plugin.mdx b/packages/core/docs/content/locales/hi-IN/plan-plugin.mdx index 2fe64411f55..50327e0e5a2 100644 --- a/packages/core/docs/content/locales/hi-IN/plan-plugin.mdx +++ b/packages/core/docs/content/locales/hi-IN/plan-plugin.mdx @@ -401,4 +401,4 @@ CI में `pnpm guard:plan-marketplace` द्वारा वेरिफ़ - [**PR विज़ुअल रीकैप**](/docs/pr-visual-recap) — हर पुल रिक्वेस्ट पर ऑटोमैटिकली `/visual-recap` चलाएं - [**Skills Guide**](/docs/skills-guide) — ऐप-समर्थित कौशल और मेनिफ़ेस्ट फ़ॉर्मैट - [**External Agents**](/docs/external-agents) — किसी भी MCP होस्ट को जोड़ें और आर्टिफ़ैक्ट्स को राउंड-ट्रिप करें - + diff --git a/packages/core/docs/content/locales/hi-IN/pr-visual-recap.mdx b/packages/core/docs/content/locales/hi-IN/pr-visual-recap.mdx index b9411160afc..3ffbd0197ee 100644 --- a/packages/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/hi-IN/pr-visual-recap.mdx @@ -212,11 +212,11 @@ PR ट्री को चेकआउट नहीं करता; यह स `VISUAL_RECAP_AGENT` रिपॉज़िटरी वेरिएबल से चुनें कि कौन-सा कोडिंग एजेंट कौशल चलाता है: -| `VISUAL_RECAP_AGENT` | कोडिंग एजेंट | ज़रूरी API कुंजी | ज़रूरी वेरिएबल | -| --------------------- | -------------------------------------- | ----------------------- | ---------------------------------------------- | -| `claude` _(डिफ़ॉल्ट)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + कम्पैटिबल प्रोवाइडर | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | कोडिंग एजेंट | ज़रूरी API कुंजी | ज़रूरी वेरिएबल | +| --------------------- | --------------------------------------- | ---------------------- | --------------------------------------------- | +| `claude` _(डिफ़ॉल्ट)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + कम्पैटिबल प्रोवाइडर | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | अगर वेरिएबल सेट नहीं है, तो Action `claude` का उपयोग करता है। @@ -305,10 +305,10 @@ DeepSeek के लिए, `VISUAL_RECAP_AGENT=openai-compatible`, ### डिफ़ॉल्ट बैकएंड के लिए सीक्रेट -| सीक्रेट | उद्देश्य | -| ------------------- | ------------------------------------------------------------------------------------------------ | +| सीक्रेट | उद्देश्य | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | `npx @agent-native/core@latest connect` द्वारा जारी किया गया रिवोकेबल टोकन। रीकैप योजना और स्क्रीनशॉट अपलोड को प्रकाशित करने के लिए अधिकृत करता है। | -| `ANTHROPIC_API_KEY` | डिफ़ॉल्ट Claude Code बैकएंड के लिए LLM कुंजी। | +| `ANTHROPIC_API_KEY` | डिफ़ॉल्ट Claude Code बैकएंड के लिए LLM कुंजी। | **टीमों के लिए: एक org सर्विस टोकन का उपयोग करें।** एक पर्सनल टोकन उस व्यक्ति से बंधा होता है जिसने उसे बनाया — अगर वे org छोड़ देते हैं या अपने टोकन रिवोक @@ -345,17 +345,17 @@ npx @agent-native/recap-cli@latest recap setup ### वैकल्पिक (सिर्फ़ अगर आप डिफ़ॉल्ट बदलते हैं) -| सीक्रेट / वेरिएबल | डिफ़ॉल्ट | कब ज़रूरत पड़ती है | -| ------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | सीक्रेट। इसके बजाय Codex के साथ रीकैप चलाने के लिए `VISUAL_RECAP_AGENT=codex` के साथ सेट करें। | -| `VISUAL_RECAP_API_KEY` | — | सीक्रेट। DeepSeek, Kimi, या किसी दूसरे OpenAI-कम्पैटिबल प्रोवाइडर के लिए `VISUAL_RECAP_AGENT=openai-compatible` के साथ सेट करें। | -| `VISUAL_RECAP_AGENT` | `claude` | वेरिएबल। कोडिंग-एजेंट बैकएंड चुनता है (`claude`, `codex`, या `openai-compatible`)। | -| `VISUAL_RECAP_BASE_URL` | कम्पैटिबल बैकएंड के लिए ज़रूरी | वेरिएबल। OpenAI-कम्पैटिबल बैकएंड के लिए HTTP(S) बेस URL; क्रेडेंशियल अस्वीकार कर दिए जाते हैं। | -| `VISUAL_RECAP_MODEL` | Claude के लिए `claude-sonnet-5`; कम्पैटिबल बैकएंड के लिए ज़रूरी | वेरिएबल। OpenAI-कम्पैटिबल बैकएंड के लिए प्रोवाइडर मॉडल id (वहां ज़रूरी)। Claude के लिए, अनसेट अब डिफ़ॉल्ट रूप से `claude-sonnet-5` उपयोग करता है — ओवरराइड करने के लिए सेट करें, जैसे सबसे सस्ते टियर के लिए `claude-haiku-4-5`। Codex के लिए, अनसेट Codex CLI का अपना डिफ़ॉल्ट उपयोग करता है। | -| `VISUAL_RECAP_REASONING` | हर मॉडल का डिफ़ॉल्ट | वेरिएबल। रीज़निंग गहराई: `none`, `minimal`, `low`, `medium`, `high`, या `xhigh`। Codex बैकएंड पर लागू होता है। | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | वेरिएबल। कॉमा-सेपरेटेड PR लेबल। सेट होने पर, गेट तब तक स्किप होता है जब तक PR पर लिस्ट किए गए लेबल में से कम-से-कम एक न हो, उदाहरण के लिए `visual recap`। | -| `RECAP_CLI_VERSION` | `latest` | वेरिएबल। वर्कफ़्लो द्वारा इंस्टॉल किए जाने वाले `@agent-native/recap-cli` वर्ज़न को पिन करता है — जैसे `1.5.0`। देखें [Version pinning](#version-pinning-copy-variant)। | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | सीक्रेट। सिर्फ़ तभी जब Plans ऐप को किसी अलग ओरिजिन पर सेल्फ-होस्ट किया जाए। | +| सीक्रेट / वेरिएबल | डिफ़ॉल्ट | कब ज़रूरत पड़ती है | +| ------------------------------ | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | सीक्रेट। इसके बजाय Codex के साथ रीकैप चलाने के लिए `VISUAL_RECAP_AGENT=codex` के साथ सेट करें। | +| `VISUAL_RECAP_API_KEY` | — | सीक्रेट। DeepSeek, Kimi, या किसी दूसरे OpenAI-कम्पैटिबल प्रोवाइडर के लिए `VISUAL_RECAP_AGENT=openai-compatible` के साथ सेट करें। | +| `VISUAL_RECAP_AGENT` | `claude` | वेरिएबल। कोडिंग-एजेंट बैकएंड चुनता है (`claude`, `codex`, या `openai-compatible`)। | +| `VISUAL_RECAP_BASE_URL` | कम्पैटिबल बैकएंड के लिए ज़रूरी | वेरिएबल। OpenAI-कम्पैटिबल बैकएंड के लिए HTTP(S) बेस URL; क्रेडेंशियल अस्वीकार कर दिए जाते हैं। | +| `VISUAL_RECAP_MODEL` | Claude के लिए `claude-sonnet-5`; कम्पैटिबल बैकएंड के लिए ज़रूरी | वेरिएबल। OpenAI-कम्पैटिबल बैकएंड के लिए प्रोवाइडर मॉडल id (वहां ज़रूरी)। Claude के लिए, अनसेट अब डिफ़ॉल्ट रूप से `claude-sonnet-5` उपयोग करता है — ओवरराइड करने के लिए सेट करें, जैसे सबसे सस्ते टियर के लिए `claude-haiku-4-5`। Codex के लिए, अनसेट Codex CLI का अपना डिफ़ॉल्ट उपयोग करता है। | +| `VISUAL_RECAP_REASONING` | हर मॉडल का डिफ़ॉल्ट | वेरिएबल। रीज़निंग गहराई: `none`, `minimal`, `low`, `medium`, `high`, या `xhigh`। Codex बैकएंड पर लागू होता है। | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | वेरिएबल। कॉमा-सेपरेटेड PR लेबल। सेट होने पर, गेट तब तक स्किप होता है जब तक PR पर लिस्ट किए गए लेबल में से कम-से-कम एक न हो, उदाहरण के लिए `visual recap`। | +| `RECAP_CLI_VERSION` | `latest` | वेरिएबल। वर्कफ़्लो द्वारा इंस्टॉल किए जाने वाले `@agent-native/recap-cli` वर्ज़न को पिन करता है — जैसे `1.5.0`। देखें [Version pinning](#version-pinning-copy-variant)। | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | सीक्रेट। सिर्फ़ तभी जब Plans ऐप को किसी अलग ओरिजिन पर सेल्फ-होस्ट किया जाए। | वर्कफ़्लो अपने-आप पता लगा लेता है कि अपने हेल्पर CLI को कैसे इनवोक करे (इस मोनोरेपो के भीतर लोकल सोर्स, कहीं और पब्लिश्ड `@agent-native/recap-cli`), @@ -489,14 +489,14 @@ GitHub उन्हें अपने camo प्रॉक्सी के ज ### फ़ोर्क वर्कफ़्लो क्या करता है और क्या नहीं करता -| वर्कफ़्लो यह करता है | वर्कफ़्लो यह नहीं करता | -| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -| **base रिपॉज़िटरी** को **base ब्रांच ref** पर चेकआउट करता है — सिर्फ़ विश्वसनीय कोड | फ़ोर्क से किसी भी कोड को चेक आउट या एग्ज़िक्यूट करना | +| वर्कफ़्लो यह करता है | वर्कफ़्लो यह नहीं करता | +| ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| **base रिपॉज़िटरी** को **base ब्रांच ref** पर चेकआउट करता है — सिर्फ़ विश्वसनीय कोड | फ़ोर्क से किसी भी कोड को चेक आउट या एग्ज़िक्यूट करना | | फ़ोर्क head को एक रिमोट ref के रूप में फ़ेच करता है (`git fetch origin pull//head:refs/recap/fork-head`) — कमिट फ़ेच करना सुरक्षित है | फ़ोर्क से पैकेज इंस्टॉल करना, फ़ोर्क स्क्रिप्ट चलाना, या फ़ोर्क सामग्री को कोड के रूप में एग्ज़ीक्यूट करना | -| `git diff base...refs/recap/fork-head` चलाता है — पहले से फ़ेच की गई दो ऑब्जेक्ट का शुद्ध टेक्स्ट diff | diff को LLM के लिए टेक्स्ट इनपुट के अलावा किसी और तरह इस्तेमाल करना | -| **base repo** का visual-recap कौशल और एजेंट कॉन्फ़िगरेशन चलाता है | फ़ोर्क से कोई कौशल या कॉन्फ़िग लोड करना | -| फ़र्स्ट-पार्टी PR जैसे ही सीक्रेट-स्कैन स्टेप (fail-closed) से diff को गुज़ारता है | सीक्रेट स्कैन को स्किप करना | -| एजेंट प्रॉम्प्ट में diff सामग्री को अविश्वसनीय बताता एक स्पष्ट prompt-hardening नोट जोड़ता है | सामान्य रीकैप एजेंट से आगे एजेंट को कोई अतिरिक्त परमिशन देना | +| `git diff base...refs/recap/fork-head` चलाता है — पहले से फ़ेच की गई दो ऑब्जेक्ट का शुद्ध टेक्स्ट diff | diff को LLM के लिए टेक्स्ट इनपुट के अलावा किसी और तरह इस्तेमाल करना | +| **base repo** का visual-recap कौशल और एजेंट कॉन्फ़िगरेशन चलाता है | फ़ोर्क से कोई कौशल या कॉन्फ़िग लोड करना | +| फ़र्स्ट-पार्टी PR जैसे ही सीक्रेट-स्कैन स्टेप (fail-closed) से diff को गुज़ारता है | सीक्रेट स्कैन को स्किप करना | +| एजेंट प्रॉम्प्ट में diff सामग्री को अविश्वसनीय बताता एक स्पष्ट prompt-hardening नोट जोड़ता है | सामान्य रीकैप एजेंट से आगे एजेंट को कोई अतिरिक्त परमिशन देना | ### आपको लेबल लगाने से पहले diff की समीक्षा क्यों करनी चाहिए @@ -535,14 +535,14 @@ GitHub उन्हें अपने camo प्रॉक्सी के ज दोबारा न लिख सके जिसे विश्वसनीय रीकैप जॉब लोड करता है और सीक्रेट एक्सफ़िल्ट्रेट कर सके: -| पाथ पैटर्न | कारण | -| ------------------------------------------ | -------------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | खुद वर्कफ़्लो | -| `**/skills/visual-(recap\|plan\|plans)/**` | वह visual-recap कौशल जिसे एजेंट फ़ॉलो करता है | -| `**/.claude/**` | एजेंट सेटिंग जो रनर लोड करता है | -| `**/CLAUDE.md` | एजेंट इंस्ट्रक्शन जो रनर लोड करता है | -| `**/AGENTS.md` | एजेंट इंस्ट्रक्शन जो रनर लोड करता है | -| `**/.mcp.json` | MCP सर्वर कॉन्फ़िग जो रनर लोड करता है | +| पाथ पैटर्न | कारण | +| ------------------------------------------ | --------------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | खुद वर्कफ़्लो | +| `**/skills/visual-(recap\|plan\|plans)/**` | वह visual-recap कौशल जिसे एजेंट फ़ॉलो करता है | +| `**/.claude/**` | एजेंट सेटिंग जो रनर लोड करता है | +| `**/CLAUDE.md` | एजेंट इंस्ट्रक्शन जो रनर लोड करता है | +| `**/AGENTS.md` | एजेंट इंस्ट्रक्शन जो रनर लोड करता है | +| `**/.mcp.json` | MCP सर्वर कॉन्फ़िग जो रनर लोड करता है | `BuilderIO/agent-native` मोनोरेपो में, वर्कफ़्लो रीकैप CLI को PR-head सोर्स के बजाय विश्वसनीय base-branch सोर्स से चलाता है। इससे `packages/core/**` @@ -669,12 +669,12 @@ Allowlist को सिर्फ़ सीक्रेट-स्कैन गे को डेलिगेट करता है। वर्कफ़्लो चलने पर हर कॉलर ऑटोमैटिकली नवीनतम लॉजिक उठा लेता है, बिना किसी लोकल अपडेट की ज़रूरत के। -| | कॉपी (डिफ़ॉल्ट) | रीयूज़ेबल | -| ---------------------------------------- | -------------------------------- | ---------------------------------------- | -| आपकी रेपो में वर्कफ़्लो का आकार | ~360 लाइनें | ~20 लाइनें | -| ऑटोमैटिकली सुधार उठाता है | नहीं — `recap setup` फिर चलाएं | हां | -| एयर-गैप / पूरी ऑडिटेबिलिटी | हां | नहीं | -| किसी ख़ास वर्ज़न पर पिन करने योग्य | सिर्फ़ लोकल रूप से एडिट करके | हां — `uses:` में `@v1.2.3` सेट करें | +| | कॉपी (डिफ़ॉल्ट) | रीयूज़ेबल | +| ---------------------------------- | ------------------------------ | ------------------------------------ | +| आपकी रेपो में वर्कफ़्लो का आकार | ~360 लाइनें | ~20 लाइनें | +| ऑटोमैटिकली सुधार उठाता है | नहीं — `recap setup` फिर चलाएं | हां | +| एयर-गैप / पूरी ऑडिटेबिलिटी | हां | नहीं | +| किसी ख़ास वर्ज़न पर पिन करने योग्य | सिर्फ़ लोकल रूप से एडिट करके | हां — `uses:` में `@v1.2.3` सेट करें | ### कॉलर स्निपेट @@ -785,4 +785,4 @@ uses: BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml@v1.2 - [Visual Plans](/docs/template-plan) — `/visual-plan` और `/visual-recap` कौशल, होस्टेड Plans कनेक्टर, और वह इंटरैक्टिव समीक्षा सतह जिस पर यह Action प्रकाशित करता है। - [Skills](/docs/skills-guide) — अपने कोडिंग एजेंट में agent-native कौशल इंस्टॉल करना। - + diff --git a/packages/core/docs/content/locales/hi-IN/template-content-local-files.mdx b/packages/core/docs/content/locales/hi-IN/template-content-local-files.mdx index 927f1128368..1efda3cace7 100644 --- a/packages/core/docs/content/locales/hi-IN/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/hi-IN/template-content-local-files.mdx @@ -21,9 +21,7 @@ history, search, संग्रह गुण, और वही action सतह
- मौजूदा स्पेस
निजी या संगठन + मौजूदा स्पेस
निजी या संगठन
नया फ़ोल्डर-आधारित स्पेस
diff --git a/packages/core/docs/content/locales/hi-IN/template-plan.mdx b/packages/core/docs/content/locales/hi-IN/template-plan.mdx index 384aecb6051..64dea7a1188 100644 --- a/packages/core/docs/content/locales/hi-IN/template-plan.mdx +++ b/packages/core/docs/content/locales/hi-IN/template-plan.mdx @@ -39,9 +39,7 @@ Codex, Claude Code, Markdown, या चिपकाई गई इम्प्
/visual-recapकोड के बाद — PR, कमिट, ब्रांच, diff + >कोड के बाद — PR, कमिट, ब्रांच, diff
diff --git a/packages/core/docs/content/locales/ja-JP/plan-plugin.mdx b/packages/core/docs/content/locales/ja-JP/plan-plugin.mdx index 47e36c4f8dd..ef4ede7bdf0 100644 --- a/packages/core/docs/content/locales/ja-JP/plan-plugin.mdx +++ b/packages/core/docs/content/locales/ja-JP/plan-plugin.mdx @@ -26,9 +26,7 @@ Plan を生成し、そのまま Plan アプリへ公開できます。このペ
- 汎用 CLI
skills add visual-plan + 汎用 CLI
skills add visual-plan
Claude Code プラグイン
- Codex プラグイン
codex plugin add + Codex プラグイン
codex plugin add
@@ -162,7 +162,8 @@ npx @agent-native/core@latest plan local serve --dir plans/ --kind plan -- >plan.mdx + canvas.mdx + prototype.mdx → localhost ブリッジ → ホスト型の Plan UI がローカルのソースを読み取る。 - publish-visual-plan を実行するまで DB への書き込みはない。publish-visual-plan を実行するまで DB + への書き込みはない。
diff --git a/packages/core/docs/content/locales/ja-JP/pr-visual-recap.mdx b/packages/core/docs/content/locales/ja-JP/pr-visual-recap.mdx index 20e54c37c46..9f136ba4652 100644 --- a/packages/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/ja-JP/pr-visual-recap.mdx @@ -193,11 +193,11 @@ npx @agent-native/recap-cli@latest recap doctor リポジトリ変数 `VISUAL_RECAP_AGENT` で、どのコーディングエージェントにスキルを実行させるかを 選びます。 -| `VISUAL_RECAP_AGENT` | コーディングエージェント | 必要な API キー | 必要な変数 | -| --------------------- | ---------------------------------------- | ----------------------- | ---------------------------------------------- | -| `claude` _(デフォルト)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + 互換プロバイダー | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | コーディングエージェント | 必要な API キー | 必要な変数 | +| ----------------------- | ------------------------------------ | ---------------------- | --------------------------------------------- | +| `claude` _(デフォルト)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + 互換プロバイダー | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | 変数が未設定の場合、この Action は `claude` を使います。 @@ -248,10 +248,10 @@ DeepSeek を使うには、`VISUAL_RECAP_AGENT=openai-compatible`、 ### デフォルトバックエンド用のシークレット -| シークレット | 用途 | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| シークレット | 用途 | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | `npx @agent-native/core@latest connect` で発行される、取り消し可能なトークンです。リキャップ Plan の公開とスクリーンショットのアップロードを許可します。 | -| `ANTHROPIC_API_KEY` | デフォルトの Claude Code バックエンド向けの LLM キーです。 | +| `ANTHROPIC_API_KEY` | デフォルトの Claude Code バックエンド向けの LLM キーです。 | **チームの場合: 組織のサービストークンを使ってください。** 個人トークンは、それを発行した本人に 紐づいています — その人が組織を離れたりトークンを取り消したりすると、そのシークレットを使う @@ -285,17 +285,17 @@ npx @agent-native/recap-cli@latest recap setup ### 任意 (デフォルトを変更する場合のみ) -| シークレット / 変数 | デフォルト | 必要になる場合 | -| ------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | シークレット。`VISUAL_RECAP_AGENT=codex` と一緒に設定すると、代わりに Codex でリキャップを実行します。 | -| `VISUAL_RECAP_API_KEY` | — | シークレット。DeepSeek、Kimi、その他の OpenAI 互換プロバイダーを使うには、`VISUAL_RECAP_AGENT=openai-compatible` と一緒に設定します。 | -| `VISUAL_RECAP_AGENT` | `claude` | 変数。コーディングエージェントのバックエンド (`claude`、`codex`、`openai-compatible` のいずれか) を選択します。 | -| `VISUAL_RECAP_BASE_URL` | 互換バックエンドでは必須 | 変数。OpenAI 互換バックエンド向けの HTTP(S) ベース URL です。認証情報を含めると拒否されます。 | -| `VISUAL_RECAP_MODEL` | Claude では `claude-sonnet-5`。互換バックエンドでは必須 | 変数。OpenAI 互換バックエンドではプロバイダーのモデル ID です (そこでは必須)。Claude では、未設定の場合は現在 `claude-sonnet-5` がデフォルトになります — 上書きするには設定してください。例えば最も安価なティアには `claude-haiku-4-5` を使います。Codex では、未設定だと Codex CLI 自身のデフォルトが使われます。 | -| `VISUAL_RECAP_REASONING` | 各モデルのデフォルト | 変数。推論の深さ: `none`、`minimal`、`low`、`medium`、`high`、`xhigh` のいずれかです。Codex バックエンドに適用されます。 | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | 変数。カンマ区切りの PR ラベルです。設定すると、PR に掲載されたラベルのうち少なくとも 1 つが付くまで、ゲートはスキップされます。例: `visual recap`。 | -| `RECAP_CLI_VERSION` | `latest` | 変数。ワークフローがインストールする `@agent-native/recap-cli` のバージョンを固定します — 例: `1.5.0`。[バージョンの固定](#version-pinning-copy-variant) を参照してください。 | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | シークレット。Plans アプリを別のオリジンでセルフホストしている場合のみ必要です。 | +| シークレット / 変数 | デフォルト | 必要になる場合 | +| ------------------------------ | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `OPENAI_API_KEY` | — | シークレット。`VISUAL_RECAP_AGENT=codex` と一緒に設定すると、代わりに Codex でリキャップを実行します。 | +| `VISUAL_RECAP_API_KEY` | — | シークレット。DeepSeek、Kimi、その他の OpenAI 互換プロバイダーを使うには、`VISUAL_RECAP_AGENT=openai-compatible` と一緒に設定します。 | +| `VISUAL_RECAP_AGENT` | `claude` | 変数。コーディングエージェントのバックエンド (`claude`、`codex`、`openai-compatible` のいずれか) を選択します。 | +| `VISUAL_RECAP_BASE_URL` | 互換バックエンドでは必須 | 変数。OpenAI 互換バックエンド向けの HTTP(S) ベース URL です。認証情報を含めると拒否されます。 | +| `VISUAL_RECAP_MODEL` | Claude では `claude-sonnet-5`。互換バックエンドでは必須 | 変数。OpenAI 互換バックエンドではプロバイダーのモデル ID です (そこでは必須)。Claude では、未設定の場合は現在 `claude-sonnet-5` がデフォルトになります — 上書きするには設定してください。例えば最も安価なティアには `claude-haiku-4-5` を使います。Codex では、未設定だと Codex CLI 自身のデフォルトが使われます。 | +| `VISUAL_RECAP_REASONING` | 各モデルのデフォルト | 変数。推論の深さ: `none`、`minimal`、`low`、`medium`、`high`、`xhigh` のいずれかです。Codex バックエンドに適用されます。 | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | 変数。カンマ区切りの PR ラベルです。設定すると、PR に掲載されたラベルのうち少なくとも 1 つが付くまで、ゲートはスキップされます。例: `visual recap`。 | +| `RECAP_CLI_VERSION` | `latest` | 変数。ワークフローがインストールする `@agent-native/recap-cli` のバージョンを固定します — 例: `1.5.0`。[バージョンの固定](#version-pinning-copy-variant) を参照してください。 | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | シークレット。Plans アプリを別のオリジンでセルフホストしている場合のみ必要です。 | このワークフローは、ヘルパー CLI をどう呼び出すかを自動検出するため (このモノレポ内では ローカルソース、それ以外では公開されている `@agent-native/recap-cli`)、設定すべき `RECAP_CLI` @@ -415,14 +415,14 @@ Plan をライトモードとダークモードの両方でスクリーンショ ### フォークワークフローが行うこと・行わないこと -| ワークフローが行うこと | ワークフローが行わないこと | -| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | -| **ベースブランチの ref** で**ベースリポジトリ**をチェックアウトする — 信頼されたコードのみ | フォークからのコードをチェックアウトまたは実行すること | +| ワークフローが行うこと | ワークフローが行わないこと | +| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **ベースブランチの ref** で**ベースリポジトリ**をチェックアウトする — 信頼されたコードのみ | フォークからのコードをチェックアウトまたは実行すること | | フォークのヘッドをリモート ref として fetch する (`git fetch origin pull//head:refs/recap/fork-head`) — コミットの fetch は安全 | フォークからパッケージをインストールする、フォークのスクリプトを実行する、フォークの内容をコードとして評価すること | -| `git diff base...refs/recap/fork-head` を実行する — すでに fetch 済みの 2 つのオブジェクトの純粋なテキスト diff | diff を LLM へのテキスト入力以外の何かとして使うこと | -| **ベースリポジトリの** visual-recap スキルとエージェント設定を実行する | フォークからスキルや設定を読み込むこと | -| ファーストパーティの PR と同じシークレットスキャンのステップ (フェイルクローズ) に diff を通す | シークレットスキャンをスキップすること | -| diff の内容を信頼できないものとして明示する、プロンプトハードニングの注記をエージェントプロンプトに追加する | 通常のリキャップエージェントを超える追加の権限をエージェントに付与すること | +| `git diff base...refs/recap/fork-head` を実行する — すでに fetch 済みの 2 つのオブジェクトの純粋なテキスト diff | diff を LLM へのテキスト入力以外の何かとして使うこと | +| **ベースリポジトリの** visual-recap スキルとエージェント設定を実行する | フォークからスキルや設定を読み込むこと | +| ファーストパーティの PR と同じシークレットスキャンのステップ (フェイルクローズ) に diff を通す | シークレットスキャンをスキップすること | +| diff の内容を信頼できないものとして明示する、プロンプトハードニングの注記をエージェントプロンプトに追加する | 通常のリキャップエージェントを超える追加の権限をエージェントに付与すること | ### ラベルを付ける前に diff をレビューしなければならない理由 @@ -456,14 +456,14 @@ Plan をライトモードとダークモードの両方でスクリーンショ これにより、PR が信頼されたリキャップジョブの読み込むワークフロー、スキル、エージェント設定を 書き換えてシークレットを窃取することは決してできません。 -| パスパターン | 理由 | -| -------------------------------------------- | ------------------------------------------ | -| `.github/workflows/pr-visual-recap.yml` | ワークフロー自体 | -| `**/skills/visual-(recap\|plan\|plans)/**` | エージェントが従う visual-recap スキル | -| `**/.claude/**` | ランナーが読み込むエージェント設定 | -| `**/CLAUDE.md` | ランナーが読み込むエージェントの指示 | -| `**/AGENTS.md` | ランナーが読み込むエージェントの指示 | -| `**/.mcp.json` | ランナーが読み込む MCP サーバー設定 | +| パスパターン | 理由 | +| ------------------------------------------ | -------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | ワークフロー自体 | +| `**/skills/visual-(recap\|plan\|plans)/**` | エージェントが従う visual-recap スキル | +| `**/.claude/**` | ランナーが読み込むエージェント設定 | +| `**/CLAUDE.md` | ランナーが読み込むエージェントの指示 | +| `**/AGENTS.md` | ランナーが読み込むエージェントの指示 | +| `**/.mcp.json` | ランナーが読み込む MCP サーバー設定 | `BuilderIO/agent-native` のモノレポでは、ワークフローは PR ヘッドのソースではなく、信頼された ベースブランチのソースからリキャップ CLI を実行します。これにより、`packages/core/**` を含む @@ -578,12 +578,12 @@ EXAMPLE_API_KEY=placeholder-value します。どの呼び出し元も、ワークフローが実行されるたびに自動的に最新のロジックを取り込むため、 ローカルでの更新は不要です。 -| | Copy (デフォルト) | Reusable | -| ---------------------------------- | -------------------------------- | ------------------------------- | -| リポジトリ内のワークフローのサイズ | 約 360 行 | 約 20 行 | -| 修正を自動的に取り込む | いいえ — `recap setup` を再実行 | はい | -| エアギャップ/完全な監査可能性 | はい | いいえ | -| 特定バージョンへの固定 | ローカルでの編集でのみ可能 | はい — `uses:` に `@v1.2.3` を設定 | +| | Copy (デフォルト) | Reusable | +| ---------------------------------- | ------------------------------- | ---------------------------------- | +| リポジトリ内のワークフローのサイズ | 約 360 行 | 約 20 行 | +| 修正を自動的に取り込む | いいえ — `recap setup` を再実行 | はい | +| エアギャップ/完全な監査可能性 | はい | いいえ | +| 特定バージョンへの固定 | ローカルでの編集でのみ可能 | はい — `uses:` に `@v1.2.3` を設定 | ### 呼び出し元のスニペット diff --git a/packages/core/docs/content/locales/ja-JP/template-content-local-files.mdx b/packages/core/docs/content/locales/ja-JP/template-content-local-files.mdx index aede2ce5ed9..c3f21244b60 100644 --- a/packages/core/docs/content/locales/ja-JP/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/ja-JP/template-content-local-files.mdx @@ -22,9 +22,7 @@ Content のデータモデルは 1 つです。すべてのページは SQL に
- 既存スペース
個人または組織 + 既存スペース
個人または組織
新しいフォルダーバックドスペース
diff --git a/packages/core/docs/content/locales/ja-JP/template-plan.mdx b/packages/core/docs/content/locales/ja-JP/template-plan.mdx index 06ea652c21d..0d29b4f3e10 100644 --- a/packages/core/docs/content/locales/ja-JP/template-plan.mdx +++ b/packages/core/docs/content/locales/ja-JP/template-plan.mdx @@ -57,7 +57,9 @@ Codex、Claude Code、Markdown、または貼り付けた実装プランを、
- コーディングエージェント
フィードバックを返す + コーディングエージェント
フィードバックを返す
``` diff --git a/packages/core/docs/content/locales/ko-KR/plan-plugin.mdx b/packages/core/docs/content/locales/ko-KR/plan-plugin.mdx index fdbfc22925b..d80e53b6b9d 100644 --- a/packages/core/docs/content/locales/ko-KR/plan-plugin.mdx +++ b/packages/core/docs/content/locales/ko-KR/plan-plugin.mdx @@ -28,9 +28,7 @@ Plan 슬래시 명령 스킬**과** 호스팅된 Plan MCP 커넥터(에이전트
- 범용 CLI
skills add visual-plan + 범용 CLI
skills add visual-plan
Claude Code 플러그인
- 추가로 정보성 Visual Recap 체크가 - 표시됩니다 — 병합을 막지 않으며 절대 필수가 아닙니다. + 추가로 정보성 Visual Recap 체크가 표시됩니다 + — 병합을 막지 않으며 절대 필수가 아닙니다.
``` @@ -131,6 +131,7 @@ npx @agent-native/core@latest skills add visual-plan --with-github-action 워크플로는 `gate`, `collect-diff`, `block-reference`, `scan`, `build-prompt`, `publish`, `shot`, `comment`, `check`, `usage`를 포함한 **게시된 CLI 하위 명령**을 `npx @agent-native/recap-cli@latest recap + `를 통해 호출하므로, 저장소에 도우미 스크립트로 복사되는 것은 없습니다. `setup`과 `doctor`는 로컬에서 실행하는 대화형 도우미이고, `gate`는 모든 요약 실행 전에 워크플로가 거치는 보안 게이트 단계입니다. @@ -210,11 +211,11 @@ GitHub가 구성된 모든 레이블과 일치하는 온라인 러너를 찾지 `VISUAL_RECAP_AGENT` 저장소 변수로 스킬을 실행할 코딩 에이전트를 선택하세요. -| `VISUAL_RECAP_AGENT` | 코딩 에이전트 | 필요한 API 키 | 필요한 변수 | -| --------------------- | ------------------------------------------ | ----------------------- | ---------------------------------------------- | -| `claude` _(기본값)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + 호환 제공업체 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | 코딩 에이전트 | 필요한 API 키 | 필요한 변수 | +| -------------------- | --------------------------------- | ---------------------- | --------------------------------------------- | +| `claude` _(기본값)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + 호환 제공업체 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | 이 변수를 설정하지 않으면 Action은 `claude`를 사용합니다. @@ -280,7 +281,7 @@ DeepSeek을 사용하려면 `VISUAL_RECAP_AGENT=openai-compatible`, `VISUAL_RECAP_BASE_URL`/`VISUAL_RECAP_MODEL`/`VISUAL_RECAP_API_KEY`를 DeepSeek, Kimi, 또는 다른 OpenAI 호환 엔드포인트로 지정하세요. - **명시적인 PR 레이블을 요구하세요.** `VISUAL_RECAP_REQUIRED_LABELS=visual - recap`을 설정하면 워크플로는 설치된 상태로 두되 PR에 해당 레이블이 +recap`을 설정하면 워크플로는 설치된 상태로 두되 PR에 해당 레이블이 붙기 전까지는 요약을 건너뜁니다. 여러 레이블이 옵트인하도록 하려면 `visual recap,recap`처럼 쉼표로 구분된 목록을 사용하세요. 목록에 있는 레이블을 붙이면 워크플로가 즉시 깨어납니다. @@ -300,10 +301,10 @@ DeepSeek을 사용하려면 `VISUAL_RECAP_AGENT=openai-compatible`, ### 기본 백엔드를 위한 시크릿 -| 시크릿 | 목적 | -| ---------------------- | ------------------------------------------------------------------------------------------ | -| `PLAN_RECAP_TOKEN` | `npx @agent-native/core@latest connect`로 발급되는 취소 가능한 토큰입니다. 요약 계획 게시와 스크린샷 업로드를 승인합니다. | -| `ANTHROPIC_API_KEY` | 기본 Claude Code 백엔드를 위한 LLM 키입니다. | +| 시크릿 | 목적 | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| `PLAN_RECAP_TOKEN` | `npx @agent-native/core@latest connect`로 발급되는 취소 가능한 토큰입니다. 요약 계획 게시와 스크린샷 업로드를 승인합니다. | +| `ANTHROPIC_API_KEY` | 기본 Claude Code 백엔드를 위한 LLM 키입니다. | **팀: 조직 서비스 토큰을 사용하세요.** 개인 토큰은 그것을 발급한 사람에게 묶여 있습니다 — 그 사람이 조직을 떠나거나 자신의 토큰을 취소하면, 그 @@ -339,17 +340,17 @@ npx @agent-native/recap-cli@latest recap setup ### 선택 사항(기본값을 바꿀 때만) -| 시크릿 / 변수 | 기본값 | 필요한 경우 | -| --------------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | 시크릿. `VISUAL_RECAP_AGENT=codex`와 함께 설정해 대신 Codex로 요약을 실행합니다. | -| `VISUAL_RECAP_API_KEY` | — | 시크릿. DeepSeek, Kimi, 또는 다른 OpenAI 호환 제공업체를 위해 `VISUAL_RECAP_AGENT=openai-compatible`과 함께 설정합니다. | -| `VISUAL_RECAP_AGENT` | `claude` | 변수. 코딩 에이전트 백엔드를 선택합니다(`claude`, `codex`, `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | 호환 백엔드에서는 필수 | 변수. OpenAI 호환 백엔드의 HTTP(S) 기본 URL입니다. 자격 증명이 포함되면 거부됩니다. | -| `VISUAL_RECAP_MODEL` | Claude는 `claude-sonnet-5`; 호환 백엔드에서는 필수 | 변수. OpenAI 호환 백엔드의 제공업체 모델 ID(그곳에서는 필수). Claude는 설정하지 않으면 이제 `claude-sonnet-5`가 기본값입니다 — 다른 값(예: 가장 저렴한 등급인 `claude-haiku-4-5`)으로 재정의하려면 설정하세요. Codex는 설정하지 않으면 Codex CLI 자체의 기본값을 사용합니다. | -| `VISUAL_RECAP_REASONING` | 각 모델의 기본값 | 변수. 추론 깊이: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Codex 백엔드에 적용됩니다. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | 변수. 쉼표로 구분된 PR 레이블입니다. 설정하면 게이트는 나열된 레이블이 하나 이상 붙을 때까지 PR을 건너뜁니다. 예: `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | 변수. 워크플로가 설치하는 `@agent-native/recap-cli` 버전을 고정합니다 — 예: `1.5.0`. [버전 고정](#version-pinning-copy-variant)을 참조하세요. | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | 시크릿. Plans 앱을 다른 origin에서 자체 호스팅할 때만 필요합니다. | +| 시크릿 / 변수 | 기본값 | 필요한 경우 | +| ------------------------------ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | 시크릿. `VISUAL_RECAP_AGENT=codex`와 함께 설정해 대신 Codex로 요약을 실행합니다. | +| `VISUAL_RECAP_API_KEY` | — | 시크릿. DeepSeek, Kimi, 또는 다른 OpenAI 호환 제공업체를 위해 `VISUAL_RECAP_AGENT=openai-compatible`과 함께 설정합니다. | +| `VISUAL_RECAP_AGENT` | `claude` | 변수. 코딩 에이전트 백엔드를 선택합니다(`claude`, `codex`, `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | 호환 백엔드에서는 필수 | 변수. OpenAI 호환 백엔드의 HTTP(S) 기본 URL입니다. 자격 증명이 포함되면 거부됩니다. | +| `VISUAL_RECAP_MODEL` | Claude는 `claude-sonnet-5`; 호환 백엔드에서는 필수 | 변수. OpenAI 호환 백엔드의 제공업체 모델 ID(그곳에서는 필수). Claude는 설정하지 않으면 이제 `claude-sonnet-5`가 기본값입니다 — 다른 값(예: 가장 저렴한 등급인 `claude-haiku-4-5`)으로 재정의하려면 설정하세요. Codex는 설정하지 않으면 Codex CLI 자체의 기본값을 사용합니다. | +| `VISUAL_RECAP_REASONING` | 각 모델의 기본값 | 변수. 추론 깊이: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Codex 백엔드에 적용됩니다. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | 변수. 쉼표로 구분된 PR 레이블입니다. 설정하면 게이트는 나열된 레이블이 하나 이상 붙을 때까지 PR을 건너뜁니다. 예: `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | 변수. 워크플로가 설치하는 `@agent-native/recap-cli` 버전을 고정합니다 — 예: `1.5.0`. [버전 고정](#version-pinning-copy-variant)을 참조하세요. | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | 시크릿. Plans 앱을 다른 origin에서 자체 호스팅할 때만 필요합니다. | 워크플로는 헬퍼 CLI를 호출하는 방식을 자동으로 감지하므로(이 모노레포 내부에서는 로컬 소스, 그 외에서는 게시된 `@agent-native/recap-cli`), @@ -482,14 +483,14 @@ GitHub 작성자 소속이 `OWNER`, `MEMBER`, `COLLABORATOR`인 신뢰된 포크 ### 포크 워크플로가 하는 일과 하지 않는 일 -| 워크플로가 하는 일 | 워크플로가 하지 않는 일 | -| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | -| **base 저장소**를 **base 브랜치 참조**에서 체크아웃 — 신뢰된 코드만 | 포크의 어떤 코드도 체크아웃하거나 실행하지 않음 | -| 포크 head를 원격 참조로 가져옴(`git fetch origin pull//head:refs/recap/fork-head`) — 커밋을 가져오는 것은 안전함 | 포크에서 패키지를 설치하거나, 포크 스크립트를 실행하거나, 포크 콘텐츠를 코드로 평가함 | -| `git diff base...refs/recap/fork-head` 실행 — 이미 가져온 두 객체 간의 순수 텍스트 diff | diff를 LLM에 대한 텍스트 입력 이외의 용도로 사용함 | -| **base 저장소**의 visual-recap 스킬과 에이전트 구성을 실행 | 포크에서 스킬이나 구성을 로드함 | -| 1차 PR과 동일하게 diff를 시크릿 스캔 단계(안전하게 실패)에 통과시킴 | 시크릿 스캔을 건너뜀 | -| diff 콘텐츠를 신뢰할 수 없는 것으로 표시하는 명시적인 프롬프트 강화 노트를 에이전트 프롬프트에 추가함 | 일반 요약 에이전트를 넘어서는 추가 권한을 에이전트에 부여함 | +| 워크플로가 하는 일 | 워크플로가 하지 않는 일 | +| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **base 저장소**를 **base 브랜치 참조**에서 체크아웃 — 신뢰된 코드만 | 포크의 어떤 코드도 체크아웃하거나 실행하지 않음 | +| 포크 head를 원격 참조로 가져옴(`git fetch origin pull//head:refs/recap/fork-head`) — 커밋을 가져오는 것은 안전함 | 포크에서 패키지를 설치하거나, 포크 스크립트를 실행하거나, 포크 콘텐츠를 코드로 평가함 | +| `git diff base...refs/recap/fork-head` 실행 — 이미 가져온 두 객체 간의 순수 텍스트 diff | diff를 LLM에 대한 텍스트 입력 이외의 용도로 사용함 | +| **base 저장소**의 visual-recap 스킬과 에이전트 구성을 실행 | 포크에서 스킬이나 구성을 로드함 | +| 1차 PR과 동일하게 diff를 시크릿 스캔 단계(안전하게 실패)에 통과시킴 | 시크릿 스캔을 건너뜀 | +| diff 콘텐츠를 신뢰할 수 없는 것으로 표시하는 명시적인 프롬프트 강화 노트를 에이전트 프롬프트에 추가함 | 일반 요약 에이전트를 넘어서는 추가 권한을 에이전트에 부여함 | ### 레이블을 붙이기 전에 diff를 검토해야 하는 이유 @@ -527,14 +528,14 @@ GitHub 작성자 소속이 `OWNER`, `MEMBER`, `COLLABORATOR`인 신뢰된 포크 스킬, 에이전트 구성을 스스로 다시 작성해서 시크릿을 유출하는 일이 결코 일어날 수 없습니다. -| 경로 패턴 | 이유 | -| -------------------------------------------- | -------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | 워크플로 자체 | -| `**/skills/visual-(recap\|plan\|plans)/**` | 에이전트가 따르는 visual-recap 스킬 | -| `**/.claude/**` | 러너가 로드하는 에이전트 설정 | -| `**/CLAUDE.md` | 러너가 로드하는 에이전트 지시 | -| `**/AGENTS.md` | 러너가 로드하는 에이전트 지시 | -| `**/.mcp.json` | 러너가 로드하는 MCP 서버 구성 | +| 경로 패턴 | 이유 | +| ------------------------------------------ | ----------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | 워크플로 자체 | +| `**/skills/visual-(recap\|plan\|plans)/**` | 에이전트가 따르는 visual-recap 스킬 | +| `**/.claude/**` | 러너가 로드하는 에이전트 설정 | +| `**/CLAUDE.md` | 러너가 로드하는 에이전트 지시 | +| `**/AGENTS.md` | 러너가 로드하는 에이전트 지시 | +| `**/.mcp.json` | 러너가 로드하는 MCP 서버 구성 | `BuilderIO/agent-native` 모노레포에서는, 워크플로가 PR head 소스가 아니라 신뢰된 base 브랜치 소스에서 요약 CLI를 실행합니다. 이 덕분에 @@ -659,12 +660,12 @@ EXAMPLE_API_KEY=placeholder-value 위임합니다. 모든 호출자는 워크플로가 실행될 때마다 자동으로 최신 로직을 가져오며, 로컬에서 업데이트할 필요가 없습니다. -| | Copy(기본값) | 재사용 가능 | -| ---------------------------------- | ---------------------------- | -------------------------- | -| 저장소 안의 워크플로 크기 | 약 360줄 | 약 20줄 | -| 수정 사항 자동 반영 | 아니오 — `recap setup` 재실행 | 예 | -| 에어갭 / 완전한 감사 가능성 | 예 | 아니오 | -| 특정 버전에 고정 가능 | 로컬 편집으로만 가능 | 예 — `uses:`에서 `@v1.2.3` 설정 | +| | Copy(기본값) | 재사용 가능 | +| --------------------------- | ----------------------------- | ------------------------------- | +| 저장소 안의 워크플로 크기 | 약 360줄 | 약 20줄 | +| 수정 사항 자동 반영 | 아니오 — `recap setup` 재실행 | 예 | +| 에어갭 / 완전한 감사 가능성 | 예 | 아니오 | +| 특정 버전에 고정 가능 | 로컬 편집으로만 가능 | 예 — `uses:`에서 `@v1.2.3` 설정 | ### 호출자 스니펫 diff --git a/packages/core/docs/content/locales/ko-KR/template-content-local-files.mdx b/packages/core/docs/content/locales/ko-KR/template-content-local-files.mdx index 4e21957c19f..5550426d96b 100644 --- a/packages/core/docs/content/locales/ko-KR/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/ko-KR/template-content-local-files.mdx @@ -22,9 +22,7 @@ Content에는 하나의 데이터 모델만 있습니다. 모든 페이지는 SQ
- 기존 공간
개인 또는 조직 + 기존 공간
개인 또는 조직
새 폴더 기반 공간
diff --git a/packages/core/docs/content/locales/ko-KR/template-plan.mdx b/packages/core/docs/content/locales/ko-KR/template-plan.mdx index 6d42759608b..390f237c093 100644 --- a/packages/core/docs/content/locales/ko-KR/template-plan.mdx +++ b/packages/core/docs/content/locales/ko-KR/template-plan.mdx @@ -32,9 +32,7 @@ Codex, Claude Code, Markdown, 또는 붙여넣은 구현 계획을 리치 텍스
/visual-plan코드 작성 전 — 아키텍처, UI, 리팩터 + >코드 작성 전 — 아키텍처, UI, 리팩터
/visual-recapPrivacidade de arquivos locaisEscreve MDX em discoplan.mdx + canvas.mdx + prototype.mdx → ponte localhost → a - UI hospedada do Plan lê a fonte local. Nenhuma gravação no banco de - dados até publish-visual-plan.plan.mdx + canvas.mdx + prototype.mdx → ponte localhost → a UI + hospedada do Plan lê a fonte local. Nenhuma gravação no banco de dados até + publish-visual-plan.
@@ -261,7 +261,7 @@ do agente para que as novas habilidades e ferramentas carreguem, e então rode `/visual-plan`. > Nota: o comando puro `npx skills@latest add BuilderIO/agent-native --skill -> visual-plan` (CLI Vercel/open Skills) instala **apenas instruções** — ele +visual-plan` (CLI Vercel/open Skills) instala **apenas instruções** — ele > não registra o conector MCP. Use o CLI do Agent-Native acima quando também > quiser o conector configurado. diff --git a/packages/core/docs/content/locales/pt-BR/pr-visual-recap.mdx b/packages/core/docs/content/locales/pt-BR/pr-visual-recap.mdx index b3af755d38a..a3a45bb7aac 100644 --- a/packages/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/pt-BR/pr-visual-recap.mdx @@ -90,8 +90,8 @@ A cada push no PR, o workflow:
Além de uma verificação informativa - Visual Recap — não bloqueante, - nunca obrigatória. + Visual Recap — não bloqueante, nunca + obrigatória.
``` @@ -228,10 +228,10 @@ Escolha qual agente de codificação roda a habilidade com a variável de repositório `VISUAL_RECAP_AGENT`: | `VISUAL_RECAP_AGENT` | Agente de codificação | Chave de API necessária | Variáveis necessárias | -| --------------------- | --------------------------------------- | ----------------------- | ---------------------------------------------- | -| `claude` _(padrão)_ | CLI do Claude Code | `ANTHROPIC_API_KEY` | — | -| `codex` | CLI do OpenAI Codex | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + provedor compatível | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | +| -------------------- | --------------------------------------- | ----------------------- | --------------------------------------------- | +| `claude` _(padrão)_ | CLI do Claude Code | `ANTHROPIC_API_KEY` | — | +| `codex` | CLI do OpenAI Codex | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + provedor compatível | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` | Se a variável não estiver definida, a action usa `claude`. @@ -328,10 +328,10 @@ repositório. ### Secrets para o backend padrão -| Secret | Finalidade | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Secret | Finalidade | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | Token revogável gerado por `npx @agent-native/core@latest connect`. Autoriza publicar o plano de recapitulação e o envio da captura de tela. | -| `ANTHROPIC_API_KEY` | A chave de LLM para o backend padrão do Claude Code. | +| `ANTHROPIC_API_KEY` | A chave de LLM para o backend padrão do Claude Code. | **Times: use um token de serviço da organização.** Um token pessoal fica vinculado à pessoa que o gerou — se ela sair da organização ou revogar seus @@ -368,17 +368,17 @@ nunca faça commit de um token real. ### Opcional (apenas se você mudar os padrões) -| Secret / variável | Padrão | Quando você precisa dele | -| ------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret. Defina junto com `VISUAL_RECAP_AGENT=codex` para rodar a recapitulação com o Codex em vez disso. | -| `VISUAL_RECAP_API_KEY` | — | Secret. Defina com `VISUAL_RECAP_AGENT=openai-compatible` para DeepSeek, Kimi ou outro provedor compatível com OpenAI. | -| `VISUAL_RECAP_AGENT` | `claude` | Variável. Seleciona o backend do agente de codificação (`claude`, `codex` ou `openai-compatible`). | -| `VISUAL_RECAP_BASE_URL` | obrigatória para o backend compatível | Variável. URL base HTTP(S) para o backend compatível com OpenAI; credenciais são rejeitadas. | -| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` para o Claude; obrigatória para o backend compatível | Variável. Id de modelo do provedor para backends compatíveis com OpenAI (obrigatório ali). Para o Claude, indefinida agora usa por padrão `claude-sonnet-5` — defina para sobrescrever, por exemplo `claude-haiku-4-5` para o tier mais barato. Para o Codex, indefinida usa o próprio padrão do CLI do Codex. | -| `VISUAL_RECAP_REASONING` | padrão de cada modelo | Variável. Profundidade de raciocínio: `none`, `minimal`, `low`, `medium`, `high` ou `xhigh`. Aplica-se ao backend Codex. | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variável. Rótulos de PR separados por vírgula. Quando definida, o portão pula até que o PR tenha ao menos um rótulo listado, por exemplo `visual recap`. | -| `RECAP_CLI_VERSION` | `latest` | Variável. Fixa a versão do `@agent-native/recap-cli` que o workflow instala — por exemplo, `1.5.0`. Veja [Fixação de versão](#version-pinning-copy-variant). | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Apenas quando o aplicativo Plans está auto-hospedado em outra origem. | +| Secret / variável | Padrão | Quando você precisa dele | +| ------------------------------ | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | Secret. Defina junto com `VISUAL_RECAP_AGENT=codex` para rodar a recapitulação com o Codex em vez disso. | +| `VISUAL_RECAP_API_KEY` | — | Secret. Defina com `VISUAL_RECAP_AGENT=openai-compatible` para DeepSeek, Kimi ou outro provedor compatível com OpenAI. | +| `VISUAL_RECAP_AGENT` | `claude` | Variável. Seleciona o backend do agente de codificação (`claude`, `codex` ou `openai-compatible`). | +| `VISUAL_RECAP_BASE_URL` | obrigatória para o backend compatível | Variável. URL base HTTP(S) para o backend compatível com OpenAI; credenciais são rejeitadas. | +| `VISUAL_RECAP_MODEL` | `claude-sonnet-5` para o Claude; obrigatória para o backend compatível | Variável. Id de modelo do provedor para backends compatíveis com OpenAI (obrigatório ali). Para o Claude, indefinida agora usa por padrão `claude-sonnet-5` — defina para sobrescrever, por exemplo `claude-haiku-4-5` para o tier mais barato. Para o Codex, indefinida usa o próprio padrão do CLI do Codex. | +| `VISUAL_RECAP_REASONING` | padrão de cada modelo | Variável. Profundidade de raciocínio: `none`, `minimal`, `low`, `medium`, `high` ou `xhigh`. Aplica-se ao backend Codex. | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | Variável. Rótulos de PR separados por vírgula. Quando definida, o portão pula até que o PR tenha ao menos um rótulo listado, por exemplo `visual recap`. | +| `RECAP_CLI_VERSION` | `latest` | Variável. Fixa a versão do `@agent-native/recap-cli` que o workflow instala — por exemplo, `1.5.0`. Veja [Fixação de versão](#version-pinning-copy-variant). | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Apenas quando o aplicativo Plans está auto-hospedado em outra origem. | O workflow detecta automaticamente como invocar seu CLI auxiliar (código fonte local dentro deste monorepo, ou o `@agent-native/recap-cli` publicado @@ -514,14 +514,14 @@ aplica, incluindo `VISUAL_RECAP_API_KEY`, `VISUAL_RECAP_BASE_URL` e ### O que o workflow de fork faz e NÃO faz -| O workflow FAZ | O workflow NÃO faz | -| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -| Faz checkout do **repositório base** na **ref da branch base** — apenas código confiável | Fazer checkout ou executar qualquer código do fork | +| O workflow FAZ | O workflow NÃO faz | +| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| Faz checkout do **repositório base** na **ref da branch base** — apenas código confiável | Fazer checkout ou executar qualquer código do fork | | Busca o head do fork como uma ref remota (`git fetch origin pull//head:refs/recap/fork-head`) — buscar commits é seguro | Instalar pacotes do fork, rodar scripts do fork, ou avaliar conteúdo do fork como código | -| Roda `git diff base...refs/recap/fork-head` — diff de texto puro entre dois objetos já buscados | Usar o diff como qualquer coisa além de entrada de texto para o LLM | -| Roda a habilidade visual-recap e a configuração de agente do **repositório base** | Carregar qualquer habilidade ou configuração do fork | -| Passa o diff pela mesma etapa de varredura de secrets (fail-closed) que PRs de primeira parte | Pular a varredura de secrets | -| Adiciona uma nota explícita de reforço de prompt ao prompt do agente, marcando o conteúdo do diff como não confiável | Conceder ao agente qualquer permissão além do agente de recapitulação normal | +| Roda `git diff base...refs/recap/fork-head` — diff de texto puro entre dois objetos já buscados | Usar o diff como qualquer coisa além de entrada de texto para o LLM | +| Roda a habilidade visual-recap e a configuração de agente do **repositório base** | Carregar qualquer habilidade ou configuração do fork | +| Passa o diff pela mesma etapa de varredura de secrets (fail-closed) que PRs de primeira parte | Pular a varredura de secrets | +| Adiciona uma nota explícita de reforço de prompt ao prompt do agente, marcando o conteúdo do diff como não confiável | Conceder ao agente qualquer permissão além do agente de recapitulação normal | ### Por que você deve revisar o diff antes de rotular @@ -564,14 +564,14 @@ um dos caminhos a seguir, para que um PR nunca possa reescrever o workflow, a habilidade ou a configuração de agente que o job confiável de recapitulação carrega e exfiltrar secrets: -| Padrão de caminho | Motivo | -| ------------------------------------------- | ------------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | O próprio workflow | -| `**/skills/visual-(recap\|plan\|plans)/**` | A habilidade visual-recap que o agente segue | -| `**/.claude/**` | Configurações de agente que o runner carrega | -| `**/CLAUDE.md` | Instruções de agente que o runner carrega | -| `**/AGENTS.md` | Instruções de agente que o runner carrega | -| `**/.mcp.json` | Configuração de servidor MCP que o runner carrega | +| Padrão de caminho | Motivo | +| ------------------------------------------ | ------------------------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | O próprio workflow | +| `**/skills/visual-(recap\|plan\|plans)/**` | A habilidade visual-recap que o agente segue | +| `**/.claude/**` | Configurações de agente que o runner carrega | +| `**/CLAUDE.md` | Instruções de agente que o runner carrega | +| `**/AGENTS.md` | Instruções de agente que o runner carrega | +| `**/.mcp.json` | Configuração de servidor MCP que o runner carrega | No monorepo `BuilderIO/agent-native`, o workflow roda o CLI de recapitulação a partir do código-fonte confiável da branch base, em vez do @@ -707,12 +707,12 @@ disso. Ele delega para `uses:`. Todo chamador recebe automaticamente a lógica mais recente quando o workflow roda, sem necessidade de atualização local. -| | Cópia (padrão) | Reutilizável | -| --------------------------------------- | ---------------------------- | ---------------------------------- | -| Tamanho do workflow no seu repositório | ~360 linhas | ~20 linhas | -| Recebe correções automaticamente | Não — rode `recap setup` de novo | Sim | -| Air-gap / auditabilidade completa | Sim | Não | -| Pode ser fixado em uma versão específica | Somente editando localmente | Sim — defina `@v1.2.3` em `uses:` | +| | Cópia (padrão) | Reutilizável | +| ---------------------------------------- | -------------------------------- | --------------------------------- | +| Tamanho do workflow no seu repositório | ~360 linhas | ~20 linhas | +| Recebe correções automaticamente | Não — rode `recap setup` de novo | Sim | +| Air-gap / auditabilidade completa | Sim | Não | +| Pode ser fixado em uma versão específica | Somente editando localmente | Sim — defina `@v1.2.3` em `uses:` | ### Trecho do chamador diff --git a/packages/core/docs/content/locales/pt-BR/template-content-local-files.mdx b/packages/core/docs/content/locales/pt-BR/template-content-local-files.mdx index 536a821c609..2ef3820573f 100644 --- a/packages/core/docs/content/locales/pt-BR/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/pt-BR/template-content-local-files.mdx @@ -108,9 +108,9 @@ ou crie um espaço privado baseado em pasta com sua própria coleção Files. ### Escolha uma política de verdade | Política | Comportamento | -| ------------------------ | -------------------------------------------------------------------------- | +| ------------------------ | ------------------------------------------------------------------------- | | `database_primary` | O Content é a autoridade; alterações na pasta são revisadas antes de usar | -| `source_primary` | A pasta é a autoridade quando não há edição concorrente no Content | +| `source_primary` | A pasta é a autoridade quando não há edição concorrente no Content | | `reviewed_bidirectional` | Alterações em qualquer direção exigem revisão em caso de conflito | diff --git a/packages/core/docs/content/locales/pt-BR/template-plan.mdx b/packages/core/docs/content/locales/pt-BR/template-plan.mdx index 5c1228fd5e3..10a0579fe88 100644 --- a/packages/core/docs/content/locales/pt-BR/template-plan.mdx +++ b/packages/core/docs/content/locales/pt-BR/template-plan.mdx @@ -258,4 +258,4 @@ nova. - [**Skills**](/docs/skills-guide) — como o Agent-Native instala habilidades - [**MCP Clients**](/docs/mcp-clients) — configurando conectores MCP hospedados - [**Templates**](/docs/cloneable-saas) — o modelo Cloneable SaaS - + diff --git a/packages/core/docs/content/locales/zh-CN/plan-plugin.mdx b/packages/core/docs/content/locales/zh-CN/plan-plugin.mdx index d2b301870b6..bd5d0d1f4a8 100644 --- a/packages/core/docs/content/locales/zh-CN/plan-plugin.mdx +++ b/packages/core/docs/content/locales/zh-CN/plan-plugin.mdx @@ -25,14 +25,10 @@ skill 实际做了什么,参见[可视化 Plans](/docs/template-plan)。
- 通用 CLI
skills add visual-plan + 通用 CLI
skills add visual-plan
- Claude Code 插件
/plugin install + Claude Code 插件
/plugin install
Codex 插件
codex plugin add @@ -161,8 +157,8 @@ Plan 应用就能持续把浏览器中的编辑保存到该仓库文件夹。Pla 本地文件隐私模式把 MDX 写入磁盘plan.mdx + canvas.mdx + prototype.mdx → 本地主机桥 → - 托管的 Plan UI 读取本地源。在调用 + >plan.mdx + canvas.mdx + prototype.mdx → 本地主机桥 → 托管的 + Plan UI 读取本地源。在调用 publish-visual-plan 之前不会写入数据库。
diff --git a/packages/core/docs/content/locales/zh-CN/pr-visual-recap.mdx b/packages/core/docs/content/locales/zh-CN/pr-visual-recap.mdx index e924ea1acc2..7bc85190959 100644 --- a/packages/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/zh-CN/pr-visual-recap.mdx @@ -52,9 +52,7 @@ PR Visual Recap 是一个 GitHub Action,它能把每个 pull request 都
- 编码代理
已配置的后端读取 diff + 编码代理
已配置的后端读取 diff
@@ -63,9 +61,7 @@ PR Visual Recap 是一个 GitHub Action,它能把每个 pull request 都
- 无头 Chrome
浅色 + 深色截图 + 无头 Chrome
浅色 + 深色截图
@@ -75,8 +71,8 @@ PR Visual Recap 是一个 GitHub Action,它能把每个 pull request 都
- 另加一个信息性的 Visual Recap 检查项 - — 不会阻塞、绝不作为必需项。 + 另加一个信息性的 Visual Recap 检查项 — + 不会阻塞、绝不作为必需项。
``` @@ -199,11 +195,11 @@ PR 的代码树;它只评估工作流逻辑和 GitHub 的 PR 元数据。一 通过 `VISUAL_RECAP_AGENT` 仓库变量选择由哪个编码代理来运行该 skill: -| `VISUAL_RECAP_AGENT` | 编码代理 | 所需的 API 密钥 | 所需的变量 | -| --------------------- | ---------------------------------------- | ----------------------- | ----------------------------------------------- | -| `claude` _(默认)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + 兼容的服务商 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | 编码代理 | 所需的 API 密钥 | 所需的变量 | +| -------------------- | -------------------------------- | ---------------------- | --------------------------------------------- | +| `claude` _(默认)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + 兼容的服务商 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | 如果没有设置该变量,该 action 会使用 `claude`。 @@ -256,10 +252,10 @@ URL 和模型 id。其他 OpenAI 兼容服务商也使用同样这三项设置 ### 默认后端所需的 secrets -| Secret | 用途 | -| ------------------- | ------------------------------------------------------------------------------------------- | +| Secret | 用途 | +| ------------------- | --------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | 由 `npx @agent-native/core@latest connect` 铸造的可撤销令牌。用于授权发布回顾计划和上传截图。 | -| `ANTHROPIC_API_KEY` | 默认 Claude Code 后端所使用的 LLM 密钥。 | +| `ANTHROPIC_API_KEY` | 默认 Claude Code 后端所使用的 LLM 密钥。 | **团队场景:使用组织服务令牌。** 个人令牌绑定在铸造它的那个人身上—— 如果他们离开组织或撤销了自己的令牌,所有使用该 secret 的仓库就会 @@ -294,17 +290,17 @@ npx @agent-native/recap-cli@latest recap setup ### 可选项(仅当你更改默认值时才需要) -| Secret / 变量 | 默认值 | 何时需要它 | -| --------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret。与 `VISUAL_RECAP_AGENT=codex` 一起设置,用于改用 Codex 运行回顾。 | -| `VISUAL_RECAP_API_KEY` | — | Secret。与 `VISUAL_RECAP_AGENT=openai-compatible` 一起设置,用于 DeepSeek、Kimi 或其他 OpenAI 兼容服务商。 | -| `VISUAL_RECAP_AGENT` | `claude` | 变量。选择编码代理后端(`claude`、`codex` 或 `openai-compatible`)。 | -| `VISUAL_RECAP_BASE_URL` | 兼容后端下为必需 | 变量。OpenAI 兼容后端的 HTTP(S) 基础 URL;不接受在其中包含凭据。 | -| `VISUAL_RECAP_MODEL` | Claude 为 `claude-sonnet-5`;兼容后端下为必需 | 变量。OpenAI 兼容后端所使用的服务商模型 id(该后端下为必需)。对于 Claude,不设置时现在默认使用 `claude-sonnet-5`——可设置以覆盖,例如设为最便宜档位的 `claude-haiku-4-5`。对于 Codex,不设置时使用 Codex CLI 自身的默认值。 | -| `VISUAL_RECAP_REASONING` | 各模型自身的默认值 | 变量。推理深度:`none`、`minimal`、`low`、`medium`、`high` 或 `xhigh`。适用于 Codex 后端。 | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | 变量。以逗号分隔的 PR 标签列表。设置后,门禁会跳过回顾,直到 PR 带上列表中至少一个标签,例如 `visual recap`。 | -| `RECAP_CLI_VERSION` | `latest` | 变量。固定工作流安装的 `@agent-native/recap-cli` 版本——例如 `1.5.0`。参见 [Version pinning](#version-pinning-copy-variant)。 | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret。仅当把 Plans 应用自托管在不同源上时才需要。 | +| Secret / 变量 | 默认值 | 何时需要它 | +| ------------------------------ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | Secret。与 `VISUAL_RECAP_AGENT=codex` 一起设置,用于改用 Codex 运行回顾。 | +| `VISUAL_RECAP_API_KEY` | — | Secret。与 `VISUAL_RECAP_AGENT=openai-compatible` 一起设置,用于 DeepSeek、Kimi 或其他 OpenAI 兼容服务商。 | +| `VISUAL_RECAP_AGENT` | `claude` | 变量。选择编码代理后端(`claude`、`codex` 或 `openai-compatible`)。 | +| `VISUAL_RECAP_BASE_URL` | 兼容后端下为必需 | 变量。OpenAI 兼容后端的 HTTP(S) 基础 URL;不接受在其中包含凭据。 | +| `VISUAL_RECAP_MODEL` | Claude 为 `claude-sonnet-5`;兼容后端下为必需 | 变量。OpenAI 兼容后端所使用的服务商模型 id(该后端下为必需)。对于 Claude,不设置时现在默认使用 `claude-sonnet-5`——可设置以覆盖,例如设为最便宜档位的 `claude-haiku-4-5`。对于 Codex,不设置时使用 Codex CLI 自身的默认值。 | +| `VISUAL_RECAP_REASONING` | 各模型自身的默认值 | 变量。推理深度:`none`、`minimal`、`low`、`medium`、`high` 或 `xhigh`。适用于 Codex 后端。 | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | 变量。以逗号分隔的 PR 标签列表。设置后,门禁会跳过回顾,直到 PR 带上列表中至少一个标签,例如 `visual recap`。 | +| `RECAP_CLI_VERSION` | `latest` | 变量。固定工作流安装的 `@agent-native/recap-cli` 版本——例如 `1.5.0`。参见 [Version pinning](#version-pinning-copy-variant)。 | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret。仅当把 Plans 应用自托管在不同源上时才需要。 | 该工作流会自动检测该如何调用它的辅助 CLI(在这个 monorepo 内部 使用本地源码,在其他地方则使用已发布的 `@agent-native/recap-cli`), @@ -426,14 +422,14 @@ PR 运行时**无法访问仓库 secrets**,工作流找不到 `PLAN_RECAP_TOKE ### 分支工作流会做什么、不会做什么 -| 工作流会做的 | 工作流不会做的 | -| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| 在**base 分支引用**处检出**base 仓库** — 只有受信任的代码 | 检出或执行分支中的任何代码 | -| 把分支的 head 拉取为一个远程引用(`git fetch origin pull//head:refs/recap/fork-head`)——拉取提交是安全的 | 从分支安装依赖包、运行分支脚本,或把分支内容当作代码执行 | -| 运行 `git diff base...refs/recap/fork-head` — 对两个已拉取对象做纯文本 diff | 把这份 diff 用作除 LLM 文本输入之外的任何用途 | -| 运行**base 仓库**自身的 visual-recap skill 和代理配置 | 从分支加载任何 skill 或配置 | -| 让 diff 经过与第一方 PR 相同的 secret 扫描步骤(失败关闭) | 跳过 secret 扫描 | -| 在代理提示词中加入一条明确的提示加固说明,把 diff 内容标记为不可信 | 授予代理超出正常回顾代理之外的任何额外权限 | +| 工作流会做的 | 工作流不会做的 | +| ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | +| 在**base 分支引用**处检出**base 仓库** — 只有受信任的代码 | 检出或执行分支中的任何代码 | +| 把分支的 head 拉取为一个远程引用(`git fetch origin pull//head:refs/recap/fork-head`)——拉取提交是安全的 | 从分支安装依赖包、运行分支脚本,或把分支内容当作代码执行 | +| 运行 `git diff base...refs/recap/fork-head` — 对两个已拉取对象做纯文本 diff | 把这份 diff 用作除 LLM 文本输入之外的任何用途 | +| 运行**base 仓库**自身的 visual-recap skill 和代理配置 | 从分支加载任何 skill 或配置 | +| 让 diff 经过与第一方 PR 相同的 secret 扫描步骤(失败关闭) | 跳过 secret 扫描 | +| 在代理提示词中加入一条明确的提示加固说明,把 diff 内容标记为不可信 | 授予代理超出正常回顾代理之外的任何额外权限 | ### 为什么打标签之前必须先审阅 diff @@ -468,14 +464,14 @@ diff 行——意图让回顾代理执行非预期的操作(例如,窃取发 PR 就永远无法重写受信任回顾任务所加载的工作流、skill 或代理配置, 从而窃取 secrets: -| 路径模式 | 原因 | -| ------------------------------------------- | ----------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | 工作流本身 | -| `**/skills/visual-(recap\|plan\|plans)/**` | 代理所遵循的 visual-recap skill | -| `**/.claude/**` | 运行器会加载的代理设置 | -| `**/CLAUDE.md` | 运行器会加载的代理指令 | -| `**/AGENTS.md` | 运行器会加载的代理指令 | -| `**/.mcp.json` | 运行器会加载的 MCP 服务器配置 | +| 路径模式 | 原因 | +| ------------------------------------------ | ------------------------------- | +| `.github/workflows/pr-visual-recap.yml` | 工作流本身 | +| `**/skills/visual-(recap\|plan\|plans)/**` | 代理所遵循的 visual-recap skill | +| `**/.claude/**` | 运行器会加载的代理设置 | +| `**/CLAUDE.md` | 运行器会加载的代理指令 | +| `**/AGENTS.md` | 运行器会加载的代理指令 | +| `**/.mcp.json` | 运行器会加载的 MCP 服务器配置 | 在 `BuilderIO/agent-native` 这个 monorepo 中,该工作流会从受信任的 base 分支源码运行 recap CLI,而不是从 PR head 的源码运行。这样 @@ -592,12 +588,12 @@ EXAMPLE_API_KEY=placeholder-value 每个调用方在工作流运行时都会自动获取最新的逻辑,不需要任何本地 更新。 -| | 复制版本(默认) | 可复用版本 | -| ---------------------------- | ------------------------------ | -------------------------------------- | -| 仓库中工作流的大小 | 约 360 行 | 约 20 行 | -| 是否自动获取修复 | 否 — 需要重新运行 `recap setup` | 是 | -| 隔离网络 / 完全可审计 | 是 | 否 | -| 是否可固定到特定版本 | 只能通过本地编辑 | 是 — 在 `uses:` 中设置 `@v1.2.3` | +| | 复制版本(默认) | 可复用版本 | +| --------------------- | ------------------------------- | -------------------------------- | +| 仓库中工作流的大小 | 约 360 行 | 约 20 行 | +| 是否自动获取修复 | 否 — 需要重新运行 `recap setup` | 是 | +| 隔离网络 / 完全可审计 | 是 | 否 | +| 是否可固定到特定版本 | 只能通过本地编辑 | 是 — 在 `uses:` 中设置 `@v1.2.3` | ### 调用方片段 diff --git a/packages/core/docs/content/locales/zh-CN/template-content-local-files.mdx b/packages/core/docs/content/locales/zh-CN/template-content-local-files.mdx index 12e29fab4cf..ad8d7527646 100644 --- a/packages/core/docs/content/locales/zh-CN/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/zh-CN/template-content-local-files.mdx @@ -17,9 +17,7 @@ Content 只有一个数据模型:每个页面都存储在 SQL 中,属于一
- 现有空间
个人或组织 + 现有空间
个人或组织
由文件夹支持的新空间
diff --git a/packages/core/docs/content/locales/zh-CN/template-plan.mdx b/packages/core/docs/content/locales/zh-CN/template-plan.mdx index 51e95d6f58c..96760454b5b 100644 --- a/packages/core/docs/content/locales/zh-CN/template-plan.mdx +++ b/packages/core/docs/content/locales/zh-CN/template-plan.mdx @@ -32,15 +32,11 @@ git diff——变成一次高空视角的可视化代码审查。两个命令打
/visual-plan写代码前 — 架构、UI、重构 + >写代码前 — 架构、UI、重构
/visual-recap写代码后 — PR、提交、分支、diff + >写代码后 — PR、提交、分支、diff
diff --git a/packages/core/docs/content/locales/zh-TW/plan-plugin.mdx b/packages/core/docs/content/locales/zh-TW/plan-plugin.mdx index b27bac39bc3..3d16a252378 100644 --- a/packages/core/docs/content/locales/zh-TW/plan-plugin.mdx +++ b/packages/core/docs/content/locales/zh-TW/plan-plugin.mdx @@ -20,14 +20,10 @@ Agent-Native **Plan** 應用程式以單一可安裝套件的形式提供。一
- 通用 CLI
skills add visual-plan + 通用 CLI
skills add visual-plan
- Claude Code 外掛
/plugin install + Claude Code 外掛
/plugin install
Codex 外掛
codex plugin add @@ -122,8 +118,8 @@ npx @agent-native/core@latest plan local serve --dir plans/ --kind plan -- 本機檔案隱私把 MDX 寫入磁碟plan.mdx + canvas.mdx + prototype.mdx → localhost 橋接 → - 託管的 Plan UI 讀取本機來源。在呼叫 + >plan.mdx + canvas.mdx + prototype.mdx → localhost 橋接 → 託管的 + Plan UI 讀取本機來源。在呼叫 publish-visual-plan 之前不會寫入資料庫。
diff --git a/packages/core/docs/content/locales/zh-TW/pr-visual-recap.mdx b/packages/core/docs/content/locales/zh-TW/pr-visual-recap.mdx index 4e9b1bc14d4..0c09918afd7 100644 --- a/packages/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +++ b/packages/core/docs/content/locales/zh-TW/pr-visual-recap.mdx @@ -35,9 +35,7 @@ PR 視覺回顧是一個 GitHub Action,會把每個 pull request 都轉變成
- 編碼代理
已設定的後端讀取 diff + 編碼代理
已設定的後端讀取 diff
@@ -46,9 +44,7 @@ PR 視覺回顧是一個 GitHub Action,會把每個 pull request 都轉變成
- 無頭 Chrome
亮色 + 暗色螢幕截圖 + 無頭 Chrome
亮色 + 暗色螢幕截圖
@@ -139,11 +135,11 @@ npx @agent-native/recap-cli@latest recap doctor 用 `VISUAL_RECAP_AGENT` 這個儲存庫變數,選擇要用哪個編碼代理來執行這個技能: -| `VISUAL_RECAP_AGENT` | 編碼代理 | 所需的 API 金鑰 | 所需的變數 | -| --------------------- | ---------------------------------------- | ------------------------ | ---------------------------------------------- | -| `claude` _(預設)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | -| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | -| `openai-compatible` | Agent-Native Code + 相容供應商 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | +| `VISUAL_RECAP_AGENT` | 編碼代理 | 所需的 API 金鑰 | 所需的變數 | +| -------------------- | ------------------------------ | ---------------------- | --------------------------------------------- | +| `claude` _(預設)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | — | +| `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — | +| `openai-compatible` | Agent-Native Code + 相容供應商 | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`、`VISUAL_RECAP_MODEL` | 如果沒有設定這個變數,這個 action 會使用 `claude`。 @@ -185,10 +181,10 @@ npx @agent-native/recap-cli@latest recap doctor ### 預設後端所需的 Secrets -| Secret | 用途 | -| ------------------- | ---------------------------------------------------------------------------------------- | +| Secret | 用途 | +| ------------------- | --------------------------------------------------------------------------------------------- | | `PLAN_RECAP_TOKEN` | 由 `npx @agent-native/core@latest connect` 鑄造的可撤銷權杖。授權發布回顧計畫與上傳螢幕截圖。 | -| `ANTHROPIC_API_KEY` | 預設 Claude Code 後端所使用的 LLM 金鑰。 | +| `ANTHROPIC_API_KEY` | 預設 Claude Code 後端所使用的 LLM 金鑰。 | **團隊:請使用組織服務權杖。** 個人權杖與鑄造它的人綁定 — 如果那個人離開組織或撤銷了他們的權杖,每個使用該 secret 的儲存庫都會開始出現 401 失敗,而且由 CI 建立的計畫,擁有者會是那個人而不是團隊。組織服務權杖則歸你的**組織**所有:它會以服務主體的身分運作(`svc-@service.`),不會因任何個人離開而失效,它所發布的回顧對整個組織可見,而且任何組織擁有者或管理員都可以列出或撤銷它。鑄造一個服務權杖(僅限組織擁有者/管理員): @@ -209,17 +205,17 @@ npx @agent-native/recap-cli@latest recap setup ### 選用(只有在你要變更預設值時才需要) -| Secret/變數 | 預設值 | 何時需要它 | -| -------------------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `OPENAI_API_KEY` | — | Secret。與 `VISUAL_RECAP_AGENT=codex` 一起設定,即可改用 Codex 執行回顧。 | -| `VISUAL_RECAP_API_KEY` | — | Secret。搭配 `VISUAL_RECAP_AGENT=openai-compatible` 設定,用於 DeepSeek、Kimi 或其他 OpenAI 相容供應商。 | -| `VISUAL_RECAP_AGENT` | `claude` | 變數。選擇編碼代理後端(`claude`、`codex` 或 `openai-compatible`)。 | -| `VISUAL_RECAP_BASE_URL` | 相容後端必填 | 變數。OpenAI 相容後端所使用的 HTTP(S) 基底網址;不接受包含憑證。 | -| `VISUAL_RECAP_MODEL` | Claude 為 `claude-sonnet-5`;相容後端必填 | 變數。OpenAI 相容後端所使用的供應商模型 id(在該情況下為必填)。對 Claude 來說,不設定現在預設為 `claude-sonnet-5` — 設定它即可覆寫,例如 `claude-haiku-4-5` 是最便宜的層級。對 Codex 來說,不設定則使用 Codex CLI 自己的預設值。 | -| `VISUAL_RECAP_REASONING` | 各模型的預設值 | 變數。推理深度:`none`、`minimal`、`low`、`medium`、`high` 或 `xhigh`。適用於 Codex 後端。 | -| `VISUAL_RECAP_REQUIRED_LABELS` | — | 變數。以逗號分隔的 PR 標籤。設定後,這個閘道會先跳過,直到 PR 至少擁有清單中的一個標籤,例如 `visual recap`。 | -| `RECAP_CLI_VERSION` | `latest` | 變數。固定這個工作流程所安裝的 `@agent-native/recap-cli` 版本 — 例如 `1.5.0`。詳見[版本固定](#version-pinning-copy-variant)。 | -| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret。只有在把 Plan 應用程式自行託管於不同來源時才需要。 | +| Secret/變數 | 預設值 | 何時需要它 | +| ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `OPENAI_API_KEY` | — | Secret。與 `VISUAL_RECAP_AGENT=codex` 一起設定,即可改用 Codex 執行回顧。 | +| `VISUAL_RECAP_API_KEY` | — | Secret。搭配 `VISUAL_RECAP_AGENT=openai-compatible` 設定,用於 DeepSeek、Kimi 或其他 OpenAI 相容供應商。 | +| `VISUAL_RECAP_AGENT` | `claude` | 變數。選擇編碼代理後端(`claude`、`codex` 或 `openai-compatible`)。 | +| `VISUAL_RECAP_BASE_URL` | 相容後端必填 | 變數。OpenAI 相容後端所使用的 HTTP(S) 基底網址;不接受包含憑證。 | +| `VISUAL_RECAP_MODEL` | Claude 為 `claude-sonnet-5`;相容後端必填 | 變數。OpenAI 相容後端所使用的供應商模型 id(在該情況下為必填)。對 Claude 來說,不設定現在預設為 `claude-sonnet-5` — 設定它即可覆寫,例如 `claude-haiku-4-5` 是最便宜的層級。對 Codex 來說,不設定則使用 Codex CLI 自己的預設值。 | +| `VISUAL_RECAP_REASONING` | 各模型的預設值 | 變數。推理深度:`none`、`minimal`、`low`、`medium`、`high` 或 `xhigh`。適用於 Codex 後端。 | +| `VISUAL_RECAP_REQUIRED_LABELS` | — | 變數。以逗號分隔的 PR 標籤。設定後,這個閘道會先跳過,直到 PR 至少擁有清單中的一個標籤,例如 `visual recap`。 | +| `RECAP_CLI_VERSION` | `latest` | 變數。固定這個工作流程所安裝的 `@agent-native/recap-cli` 版本 — 例如 `1.5.0`。詳見[版本固定](#version-pinning-copy-variant)。 | +| `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret。只有在把 Plan 應用程式自行託管於不同來源時才需要。 | 這個工作流程會自動偵測要如何呼叫它的輔助 CLI(在這個 monorepo 內部使用本機原始碼,其他地方則使用已發布的 `@agent-native/recap-cli`),因此沒有 `RECAP_CLI` 這個變數需要設定。 @@ -315,14 +311,14 @@ npx @agent-native/recap-cli@latest recap setup ### fork 工作流程會做什麼,以及絕不會做什麼 -| 這個工作流程「會」做的事 | 這個工作流程「絕不會」做的事 | -| --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 簽出 **base 儲存庫**在 **base 分支參照**上的內容 — 僅限受信任的程式碼 | 簽出或執行任何來自 fork 的程式碼 | -| 把 fork 的 head 當作遠端參照抓取(`git fetch origin pull//head:refs/recap/fork-head`) — 抓取提交是安全的 | 從 fork 安裝套件、執行 fork 的指令碼,或把 fork 的內容當成程式碼來執行 | -| 執行 `git diff base...refs/recap/fork-head` — 純粹是兩個已抓取物件之間的文字 diff | 把這個 diff 當成除了 LLM 文字輸入以外的任何用途 | -| 執行 **base 儲存庫**的 visual-recap 技能與代理設定 | 從 fork 載入任何技能或設定 | -| 讓這個 diff 通過與第一方 PR 相同的 secret 掃描步驟(失敗封閉) | 略過 secret 掃描 | -| 在代理提示中加入一則明確的提示強化備註,把 diff 內容標記為不受信任 | 授予代理超出一般回顧代理範圍之外的任何額外權限 | +| 這個工作流程「會」做的事 | 這個工作流程「絕不會」做的事 | +| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | +| 簽出 **base 儲存庫**在 **base 分支參照**上的內容 — 僅限受信任的程式碼 | 簽出或執行任何來自 fork 的程式碼 | +| 把 fork 的 head 當作遠端參照抓取(`git fetch origin pull//head:refs/recap/fork-head`) — 抓取提交是安全的 | 從 fork 安裝套件、執行 fork 的指令碼,或把 fork 的內容當成程式碼來執行 | +| 執行 `git diff base...refs/recap/fork-head` — 純粹是兩個已抓取物件之間的文字 diff | 把這個 diff 當成除了 LLM 文字輸入以外的任何用途 | +| 執行 **base 儲存庫**的 visual-recap 技能與代理設定 | 從 fork 載入任何技能或設定 | +| 讓這個 diff 通過與第一方 PR 相同的 secret 掃描步驟(失敗封閉) | 略過 secret 掃描 | +| 在代理提示中加入一則明確的提示強化備註,把 diff 內容標記為不受信任 | 授予代理超出一般回顧代理範圍之外的任何額外權限 | ### 為什麼你必須先審閱 diff 才能套用標籤 @@ -344,14 +340,14 @@ fork 的 diff 是攻擊者可控制的文字,回顧代理會把它當成輸入 當 PR 觸及以下任何路徑時,`gate` 步驟會完全跳過這次回顧,這樣一個 PR 就永遠無法改寫受信任回顧工作所載入的工作流程、技能或代理設定,藉此外洩 secrets: -| 路徑模式 | 原因 | -| -------------------------------------------- | ---------------------------------------- | -| `.github/workflows/pr-visual-recap.yml` | 這個工作流程本身 | -| `**/skills/visual-(recap\|plan\|plans)/**` | 代理所依循的 visual-recap 技能 | -| `**/.claude/**` | runner 載入的代理設定 | -| `**/CLAUDE.md` | runner 載入的代理指令 | -| `**/AGENTS.md` | runner 載入的代理指令 | -| `**/.mcp.json` | runner 載入的 MCP 伺服器設定 | +| 路徑模式 | 原因 | +| ------------------------------------------ | ------------------------------ | +| `.github/workflows/pr-visual-recap.yml` | 這個工作流程本身 | +| `**/skills/visual-(recap\|plan\|plans)/**` | 代理所依循的 visual-recap 技能 | +| `**/.claude/**` | runner 載入的代理設定 | +| `**/CLAUDE.md` | runner 載入的代理指令 | +| `**/AGENTS.md` | runner 載入的代理指令 | +| `**/.mcp.json` | runner 載入的 MCP 伺服器設定 | 在 `BuilderIO/agent-native` 這個 monorepo 中,這個工作流程會從受信任的 base 分支原始碼執行回顧 CLI,而不是 PR head 的原始碼。這樣可以讓一般的套件變更(包括 `packages/core/**`)依然能進行回顧,同時不會執行由 PR 修改過的 CLI 程式碼。 @@ -432,12 +428,12 @@ EXAMPLE_API_KEY=placeholder-value **可重複使用**選項則會改寫入一個精簡的、約 20 行的呼叫端。它會透過 `uses:` 委派給 `BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml`。每個呼叫端在工作流程執行時,都會自動取得最新的邏輯,不需要任何本機更新。 -| | 複製(預設) | 可重複使用 | -| ------------------------------ | ---------------------------- | -------------------------------- | -| 你儲存庫中的工作流程大小 | 約 360 行 | 約 20 行 | -| 自動取得修正 | 否 — 需重新執行 `recap setup` | 是 | -| 氣隙隔離/完整可稽核性 | 是 | 否 | -| 可固定到特定版本 | 只能透過本機編輯 | 是 — 在 `uses:` 中設定 `@v1.2.3` | +| | 複製(預設) | 可重複使用 | +| ------------------------ | ----------------------------- | -------------------------------- | +| 你儲存庫中的工作流程大小 | 約 360 行 | 約 20 行 | +| 自動取得修正 | 否 — 需重新執行 `recap setup` | 是 | +| 氣隙隔離/完整可稽核性 | 是 | 否 | +| 可固定到特定版本 | 只能透過本機編輯 | 是 — 在 `uses:` 中設定 `@v1.2.3` | ### 呼叫端片段 diff --git a/packages/core/docs/content/locales/zh-TW/template-content-local-files.mdx b/packages/core/docs/content/locales/zh-TW/template-content-local-files.mdx index a3196299653..0960408f75b 100644 --- a/packages/core/docs/content/locales/zh-TW/template-content-local-files.mdx +++ b/packages/core/docs/content/locales/zh-TW/template-content-local-files.mdx @@ -85,11 +85,11 @@ SQL 只儲存不透明的連線 ID、相對路徑、雜湊與來源中繼資料 ### 選擇真實來源政策 -| 政策 | 行為 | -| ------------------------ | --------------------------------------------- | +| 政策 | 行為 | +| ------------------------ | ---------------------------------------------------- | | `database_primary` | Content 為主要真實來源;資料夾的變更會先經審查才套用 | | `source_primary` | 在沒有並行的 Content 編輯時,以資料夾為主要真實來源 | -| `reviewed_bidirectional` | 任一方向的變更,發生衝突時都需要經過審查 | +| `reviewed_bidirectional` | 任一方向的變更,發生衝突時都需要經過審查 | diff --git a/packages/core/docs/content/locales/zh-TW/template-plan.mdx b/packages/core/docs/content/locales/zh-TW/template-plan.mdx index 8e9d2760ba5..da9205588c0 100644 --- a/packages/core/docs/content/locales/zh-TW/template-plan.mdx +++ b/packages/core/docs/content/locales/zh-TW/template-plan.mdx @@ -25,15 +25,11 @@ Agent-Native Plan 是編碼代理的視覺化計畫模式。它會把一份普
/visual-plan寫程式碼之前 — 架構、UI、重構 + >寫程式碼之前 — 架構、UI、重構
/visual-recap寫程式碼之後 — PR、提交、分支、diff + >寫程式碼之後 — PR、提交、分支、diff
diff --git a/packages/core/docs/content/template-plan-automations.mdx b/packages/core/docs/content/template-plan-automations.mdx index 0e70de5136e..2b41e7f5e9c 100644 --- a/packages/core/docs/content/template-plan-automations.mdx +++ b/packages/core/docs/content/template-plan-automations.mdx @@ -20,29 +20,29 @@ The Plan template emits four events. Any automation can subscribe to them. Fires when a new visual plan or recap is created. | Field | Type | Description | -| ----------- | --------------------- | ----------------------------------------- | -| `planId` | string | Unique plan identifier | -| `title` | string | Plan title | -| `kind` | `"plan"` \| `"recap"` | Whether this is a plan or a recap | -| `status` | string | Initial status (e.g. `"review"`) | +| ----------- | --------------------- | ---------------------------------------- | +| `planId` | string | Unique plan identifier | +| `title` | string | Plan title | +| `kind` | `"plan"` \| `"recap"` | Whether this is a plan or a recap | +| `status` | string | Initial status (e.g. `"review"`) | | `path` | string | App-relative path (e.g. `/plans/plan-…`) | -| `createdBy` | string | Always `"agent"` for plan creation | +| `createdBy` | string | Always `"agent"` for plan creation | #### `plan.commented` Fires when one or more comments are added to a plan. | Field | Type | Description | -| ------------------ | -------------------------------- | ------------------------------------------------------------- | -| `planId` | string | Plan identifier | -| `title` | string | Plan title | -| `kind` | `"plan"` \| `"recap"` | Plan or recap | -| `commentIds` | string[] | IDs of the new comments | -| `commentCount` | number | Number of new comments in this batch | +| ------------------ | -------------------------------- | ----------------------------------------------------------- | +| `planId` | string | Plan identifier | +| `title` | string | Plan title | +| `kind` | `"plan"` \| `"recap"` | Plan or recap | +| `commentIds` | string[] | IDs of the new comments | +| `commentCount` | number | Number of new comments in this batch | | `resolutionTarget` | `"agent"` \| `"human"` \| `null` | Dominant target — `"agent"` if any comment targets an agent | -| `excerpt` | string | First 200 characters of the first comment | -| `author` | string \| null | Email of the commenter, if known | -| `path` | string | App-relative path | +| `excerpt` | string | First 200 characters of the first comment | +| `author` | string \| null | Email of the commenter, if known | +| `path` | string | App-relative path | #### `plan.published` @@ -50,27 +50,27 @@ Fires when a local plan is published (or re-published) to a hosted shareable URL. | Field | Type | Description | -| --------------------- | --------------------- | ------------------------------------ | -| `planId` | string | Local plan identifier | -| `title` | string | Plan title | -| `kind` | `"plan"` \| `"recap"` | Plan or recap | -| `hostedPlanId` | string | Hosted plan identifier | -| `url` | string | Full public URL of the hosted plan | -| `requestedVisibility` | string | `"public"`, `"private"`, etc. | +| --------------------- | --------------------- | ---------------------------------- | +| `planId` | string | Local plan identifier | +| `title` | string | Plan title | +| `kind` | `"plan"` \| `"recap"` | Plan or recap | +| `hostedPlanId` | string | Hosted plan identifier | +| `url` | string | Full public URL of the hosted plan | +| `requestedVisibility` | string | `"public"`, `"private"`, etc. | #### `plan.status.changed` Fires when a plan's status changes (e.g. `review` → `approved`). | Field | Type | Description | -| ----------- | --------------------- | ------------------------------------ | -| `planId` | string | Plan identifier | -| `title` | string | Plan title | -| `kind` | `"plan"` \| `"recap"` | Plan or recap | -| `oldStatus` | string \| null | Previous status | -| `newStatus` | string | New status | -| `changedBy` | string \| null | Email of the person who changed it | -| `path` | string | App-relative path | +| ----------- | --------------------- | ---------------------------------- | +| `planId` | string | Plan identifier | +| `title` | string | Plan title | +| `kind` | `"plan"` \| `"recap"` | Plan or recap | +| `oldStatus` | string \| null | Previous status | +| `newStatus` | string | New status | +| `changedBy` | string \| null | Email of the person who changed it | +| `path` | string | App-relative path | diff --git a/packages/core/docs/content/template-plan-developers.mdx b/packages/core/docs/content/template-plan-developers.mdx index 8d2ff1b7aba..927ca97d35d 100644 --- a/packages/core/docs/content/template-plan-developers.mdx +++ b/packages/core/docs/content/template-plan-developers.mdx @@ -77,17 +77,17 @@ local persistence, or running a fully self-hosted review surface. Schema lives in `templates/plan/server/db/schema.ts`. Core tables: -| Table | What it holds | -| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Table | What it holds | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `plans` | Each plan or recap — `title`, `brief`, `kind` (plan/recap), `status`, `source`, `html`/`markdown`/`content`, `hosted_plan_id`/`url`, usage stats, `source_url`, `deleted_at`/`deleted_by` | -| `plan_sections` | Ordered sections within a plan — `type`, `title`, `body`, `html`, `sort_order`, `created_by` | -| `plan_comments` | Threaded comments — `parent_comment_id`, `kind`, `status`, `anchor`, `message`, `author_email`, `resolution_target`, `mentions_json`, `resolved_by` | -| `plan_events` | Audit log of agent/human events on a plan — `type`, `message`, JSON `payload`, `created_by` | -| `plan_reports` | Abuse reports on a public plan — `reason`, `status`, `reporter_email`, `occurrence_count` | -| `plan_versions` | Point-in-time snapshots for version history — `snapshot_json`, `change_label`, plus denormalized `block_count`/`section_count`/`preview_text` for fast listing | -| `plan_shares` | Per-principal share grants (viewer / editor / admin) | -| `plan_guest_mints` | Rate-limit records for guest session issuance | -| `plan_assets` | Inline image assets stored as base64 (fallback when no upload provider) | +| `plan_sections` | Ordered sections within a plan — `type`, `title`, `body`, `html`, `sort_order`, `created_by` | +| `plan_comments` | Threaded comments — `parent_comment_id`, `kind`, `status`, `anchor`, `message`, `author_email`, `resolution_target`, `mentions_json`, `resolved_by` | +| `plan_events` | Audit log of agent/human events on a plan — `type`, `message`, JSON `payload`, `created_by` | +| `plan_reports` | Abuse reports on a public plan — `reason`, `status`, `reporter_email`, `occurrence_count` | +| `plan_versions` | Point-in-time snapshots for version history — `snapshot_json`, `change_label`, plus denormalized `block_count`/`section_count`/`preview_text` for fast listing | +| `plan_shares` | Per-principal share grants (viewer / editor / admin) | +| `plan_guest_mints` | Rate-limit records for guest session issuance | +| `plan_assets` | Inline image assets stored as base64 (fallback when no upload provider) |