v0.42.37.0 fix(security,ingest): source-isolation grant enforcement + non-string frontmatter guard + papercuts (#1999)
* fix(security): scope cross-source reads to the caller grant; close get_page exact-path leak One shared resolveRequestedScope() routes every source-scoped read op (query, code_callers/callees, search_by_image, code_blast/flow, get_page) through a single fail-closed trust+grant ladder: a remote caller's __all__ collapses to its granted sources (never the whole brain) and an explicit out-of-grant source_id is rejected. get_page's exact-match path now honors a federated grant via getPage(sourceIds[]) in both engines. Legacy bearer tokens carry their stored permissions.source_id grant (bounded, never widened). Also retries getConfig on transient connection loss. Closes #1924, #1371, #1393, #1336, #1603. * fix(ingest): non-string frontmatter no longer aborts lint/sync; embed/hook/catalog papercuts Parser coerces a non-string title to a string and falls back to inference for slug/type (never fabricating a "123" slug), with a lint NON_STRING_FIELD finding surfacing the malformed frontmatter; a defensive guard in content-sanity stops a non-string title from crashing the whole lint/sync run brain-wide. Plus: embed --catch-up no longer arms the overflowed 32-bit budget timer (and surfaces unembeddable chunks); the frontmatter pre-commit hook ships a correct .md/.mdx regex; and the skill catalog parses YAML block-scalar descriptions. Closes #1883, #1658, #1556, #1948, #1946, #1840, #1711. * v0.42.37.0 fix(security,ingest): source-isolation grant enforcement + non-string frontmatter guard + papercuts Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: add NON_STRING_FIELD frontmatter validation class to docs for v0.42.37.0 The v0.42.37.0 non-string-frontmatter fix added an eighth validation class (NON_STRING_FIELD / lint code frontmatter-non-string-field). Update the two current-state docs that enumerate the validation classes: - skills/frontmatter-guard/SKILL.md (seven->eight + table row) - docs/integrations/pre-commit.md (seven->eight + table row) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -895,13 +895,21 @@ export class PostgresEngine implements BrainEngine {
|
||||
}
|
||||
|
||||
// Pages CRUD
|
||||
async getPage(slug: string, opts?: { sourceId?: string; includeDeleted?: boolean }): Promise<Page | null> {
|
||||
async getPage(slug: string, opts?: { sourceId?: string; sourceIds?: string[]; includeDeleted?: boolean }): Promise<Page | null> {
|
||||
const sql = this.sql;
|
||||
const includeDeleted = opts?.includeDeleted === true;
|
||||
const sourceId = opts?.sourceId;
|
||||
// v0.26.5: default hides soft-deleted rows. Compose with optional sourceId
|
||||
const sourceIds = opts?.sourceIds;
|
||||
// v0.26.5: default hides soft-deleted rows. Compose with optional source
|
||||
// filter via fragment chaining (postgres.js supports sql`` composition).
|
||||
const sourceCondition = sourceId ? sql`AND source_id = ${sourceId}` : sql``;
|
||||
// #1393: a federated grant (sourceIds[]) takes precedence over scalar
|
||||
// sourceId so the exact-match read honors allowedSources, not just one source.
|
||||
const sourceCondition =
|
||||
sourceIds && sourceIds.length > 0
|
||||
? sql`AND source_id = ANY(${sourceIds}::text[])`
|
||||
: sourceId
|
||||
? sql`AND source_id = ${sourceId}`
|
||||
: sql``;
|
||||
const deletedCondition = includeDeleted ? sql`` : sql`AND deleted_at IS NULL`;
|
||||
const rows = await sql`
|
||||
SELECT id, source_id, slug, type, title, compiled_truth, timeline, frontmatter, content_hash, created_at, updated_at, deleted_at,
|
||||
@@ -4847,9 +4855,26 @@ export class PostgresEngine implements BrainEngine {
|
||||
|
||||
// Config
|
||||
async getConfig(key: string): Promise<string | null> {
|
||||
const sql = this.sql;
|
||||
const rows = await sql`SELECT value FROM config WHERE key = ${key}`;
|
||||
return rows.length > 0 ? (rows[0].value as string) : null;
|
||||
// #1603: a transient pooler drop on this read used to throw / fall through
|
||||
// to defaults silently — which on remote Postgres surfaces as the wrong
|
||||
// search mode/knobs and empty-stdout queries. Retry-with-reconnect using the
|
||||
// same tuned opts as the bulk writers. No auditSite: this is a single-row
|
||||
// read, not a bulk write, so it must not emit batch-retry audit rows.
|
||||
// `this.sql` is a getter, so each attempt sees the pool rebuilt by reconnect.
|
||||
const opts = this.getBulkRetryOpts();
|
||||
return withRetry(
|
||||
async () => {
|
||||
const rows = await this.sql`SELECT value FROM config WHERE key = ${key}`;
|
||||
return rows.length > 0 ? (rows[0].value as string) : null;
|
||||
},
|
||||
{
|
||||
maxRetries: opts.maxRetries,
|
||||
delayMs: opts.delayMs,
|
||||
delayMaxMs: opts.delayMaxMs,
|
||||
jitter: BULK_RETRY_OPTS.jitter,
|
||||
reconnect: (ctx) => this.reconnect(ctx),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async setConfig(key: string, value: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user