Publish DevRunbook source
Managed validation / full (push) Successful in 3m18s

This commit is contained in:
DevRunbook release export
2026-09-03 04:09:17 +02:00
commit cfd2804e27
928 changed files with 161642 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@devrunbook/worker",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json && esbuild src/index.ts src/operator/password-reset.ts src/operator/artifact-retention.ts --bundle --platform=node --format=esm --target=node24 --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --outdir=dist --outbase=src",
"dev": "tsx watch src/index.ts",
"lint": "eslint src --max-warnings=0",
"operator:password-reset": "tsx src/operator/password-reset.ts",
"operator:retention": "tsx src/operator/artifact-retention.ts",
"start": "node dist/index.js",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@devrunbook/application": "workspace:*",
"@devrunbook/artifacts": "workspace:*",
"@devrunbook/config": "workspace:*",
"@devrunbook/content": "workspace:*",
"@devrunbook/db": "workspace:*",
"@devrunbook/integrations": "workspace:*",
"@devrunbook/observability": "workspace:*"
},
"devDependencies": {
"@types/node": "24.13.3",
"esbuild": "0.28.1",
"tsx": "4.20.6",
"typescript": "5.9.3",
"vitest": "4.1.10"
}
}
+101
View File
@@ -0,0 +1,101 @@
import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import type { BuiltInPlaybookImportRecord } from '@devrunbook/application'
import {
resolveBuiltInCatalogPaths,
synchronizeBuiltInCatalog,
} from './built-in-catalog'
function records(): BuiltInPlaybookImportRecord[] {
return Array.from({ length: 28 }, (_, index) => ({
logicalId: `id-${index}`,
slug: `slug-${index}`,
namespace: 'builtin',
sourceType: 'built_in',
semanticVersion: '1.0.0',
lifecycle: 'reviewed',
packageApiVersion: 'devrunbook.io/v1alpha1',
title: `Title ${index}`,
summary: 'Summary',
category: 'testing',
riskTier: 'low',
packageJson: {},
templateText: 'Prompt\n',
contentDigest: index.toString(16).padStart(64, '0'),
searchProjection: { searchText: `Title ${index}` },
}))
}
describe('worker built-in catalog synchronization', () => {
it('supports the repository layout used by the development container', () => {
const repositoryCatalog = path.resolve(
'/app/content',
'../catalog/seed-catalog.yaml',
)
expect(
resolveBuiltInCatalogPaths(
'/app/content',
(candidate) => candidate === repositoryCatalog,
),
).toEqual({
packageRoot: path.resolve('/app/content', 'playbooks'),
seedCatalogPath: repositoryCatalog,
})
})
it('validates, persists and safely logs counts before polling can begin', async () => {
const logger = { info: vi.fn() }
const store = {
importBuiltIns: vi.fn().mockResolvedValue({
total: 28,
insertedPlaybooks: 0,
insertedVersions: 0,
unchangedVersions: 28,
}),
}
await expect(
synchronizeBuiltInCatalog({
logger,
contentRoot: '/canonical-content',
load: async (contentRoot, seedCatalogPath) => {
expect(contentRoot).toBe(
path.resolve('/canonical-content', 'playbooks'),
)
expect(seedCatalogPath).toBe(
path.resolve('/canonical-content', 'catalog/seed-catalog.yaml'),
)
return records()
},
store,
}),
).resolves.toMatchObject({ total: 28, unchangedVersions: 28 })
expect(store.importBuiltIns).toHaveBeenCalledOnce()
expect(logger.info).toHaveBeenCalledWith(
{
total: 28,
insertedPlaybooks: 0,
insertedVersions: 0,
unchangedVersions: 28,
},
'built-in catalog synchronized',
)
})
it('does not persist an incomplete canonical load', async () => {
const store = { importBuiltIns: vi.fn() }
await expect(
synchronizeBuiltInCatalog({
logger: { info: vi.fn() },
load: async () => records().slice(0, 27),
store,
}),
).rejects.toMatchObject({ code: 'catalog_import_incomplete' })
expect(store.importBuiltIns).not.toHaveBeenCalled()
})
})
+68
View File
@@ -0,0 +1,68 @@
import { existsSync } from 'node:fs'
import path from 'node:path'
import {
importBuiltInPlaybooks,
type BuiltInPlaybookImportRecord,
type BuiltInPlaybookImportResult,
type BuiltInPlaybookImportStore,
} from '@devrunbook/application'
import { loadBuiltInPlaybookRecords } from '@devrunbook/content'
import { DrizzleBuiltInPlaybookImporter } from '@devrunbook/db'
interface SafeCatalogLogger {
info(bindings: Record<string, number>, message: string): void
}
export interface SynchronizeBuiltInCatalogOptions {
readonly logger: SafeCatalogLogger
readonly contentRoot?: string
readonly load?: (
contentRoot?: string,
seedCatalogPath?: string,
) => Promise<readonly BuiltInPlaybookImportRecord[]>
readonly store?: BuiltInPlaybookImportStore
}
export function resolveBuiltInCatalogPaths(
contentRoot: string,
pathExists: (candidate: string) => boolean = existsSync,
): { packageRoot: string; seedCatalogPath: string } {
const packageRoot = path.resolve(contentRoot, 'playbooks')
const seedCatalogCandidates = [
path.resolve(contentRoot, 'catalog/seed-catalog.yaml'),
path.resolve(contentRoot, '../catalog/seed-catalog.yaml'),
]
return {
packageRoot,
seedCatalogPath:
seedCatalogCandidates.find((candidate) => pathExists(candidate)) ??
seedCatalogCandidates[0]!,
}
}
export async function synchronizeBuiltInCatalog(
options: SynchronizeBuiltInCatalogOptions,
): Promise<BuiltInPlaybookImportResult> {
const paths = options.contentRoot
? resolveBuiltInCatalogPaths(options.contentRoot)
: undefined
const records = await (options.load ?? loadBuiltInPlaybookRecords)(
paths?.packageRoot,
paths?.seedCatalogPath,
)
const result = await importBuiltInPlaybooks(
options.store ?? new DrizzleBuiltInPlaybookImporter(),
records,
)
options.logger.info(
{
total: result.total,
insertedPlaybooks: result.insertedPlaybooks,
insertedVersions: result.insertedVersions,
unchangedVersions: result.unchangedVersions,
},
'built-in catalog synchronized',
)
return result
}
+100
View File
@@ -0,0 +1,100 @@
import { randomUUID } from 'node:crypto'
import { hostname } from 'node:os'
import { pathToFileURL } from 'node:url'
import { parseEnvironment } from '@devrunbook/config'
import {
closeDatabase,
PostgresJobStore,
RepositoryRefreshScheduler,
} from '@devrunbook/db'
import { createLogger } from '@devrunbook/observability'
import { synchronizeBuiltInCatalog } from './built-in-catalog'
import { createJobHandlers } from './jobs/handlers'
import { createGiteaSnapshotDependencies } from './jobs/gitea-snapshot-dependencies'
import { runWorkerLoop } from './jobs/worker-loop'
export async function main(
environment: Record<string, string | undefined> = process.env,
): Promise<void> {
const config = parseEnvironment(environment)
const logger = createLogger(config.LOG_LEVEL)
const controller = new AbortController()
const workerId = `${hostname()}:${process.pid}:${randomUUID()}`
const shutdown = () => controller.abort()
process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)
try {
await synchronizeBuiltInCatalog({
logger,
contentRoot: config.CONTENT_ROOT,
})
logger.info(
{
pollIntervalMs: config.WORKER_POLL_INTERVAL_MS,
leaseSeconds: config.JOB_LEASE_SECONDS,
repositoryRefreshScheduleMs: config.REPOSITORY_REFRESH_SCHEDULE_MS,
repositoryStaleAfterHours: config.REPOSITORY_STALE_AFTER_HOURS,
},
'worker started',
)
const repositoryRefreshScheduler = new RepositoryRefreshScheduler()
await runWorkerLoop({
store: new PostgresJobStore(),
handlers: createJobHandlers(
() => new Date(),
createGiteaSnapshotDependencies(config),
),
workerId,
nextLeaseId: randomUUID,
leaseDurationMs: config.JOB_LEASE_SECONDS * 1_000,
pollIntervalMs: config.WORKER_POLL_INTERVAL_MS,
signal: controller.signal,
scheduleIntervalMs: config.REPOSITORY_REFRESH_SCHEDULE_MS,
schedule: async () => {
const result = await repositoryRefreshScheduler.plan({
staleAfterHours: config.REPOSITORY_STALE_AFTER_HOURS,
})
if (result.queued > 0)
logger.info(result, 'repository refreshes planned')
},
onScheduleError: (error) => {
logger.error(
{ errorType: error instanceof Error ? error.name : 'unknown' },
'repository refresh planning failed',
)
},
onResult: (result) => {
if (result.outcome !== 'idle') {
logger.info(
{
outcome: result.outcome,
jobId: result.jobId,
jobType: result.jobType,
errorCode: result.errorCode,
},
'worker job processed',
)
}
},
onPollError: (error) => {
logger.error(
{ errorType: error instanceof Error ? error.name : 'unknown' },
'worker poll failed',
)
},
})
} finally {
process.removeListener('SIGINT', shutdown)
process.removeListener('SIGTERM', shutdown)
await closeDatabase()
logger.info('worker stopped')
}
}
const entryPoint = process.argv[1]
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
await main()
}
@@ -0,0 +1,98 @@
import type { parseEnvironment } from '@devrunbook/config'
import {
DrizzleGiteaIntegrationStore,
RepositorySnapshotStore,
} from '@devrunbook/db'
import {
decryptIntegrationSecret,
GiteaAdapter,
GiteaClient,
type GiteaNetworkPolicy,
type IntegrationKeyRing,
type SecretEnvelopeV1,
} from '@devrunbook/integrations'
import type { RepositorySnapshotDependencies } from './repository-snapshot'
type Configuration = ReturnType<typeof parseEnvironment>
function allowedHosts(value: string): readonly string[] {
return Object.freeze([
...new Set(
value
.split(',')
.map((host) => host.trim())
.filter(Boolean),
),
])
}
function keyRing(config: Configuration): IntegrationKeyRing {
return {
activeVersion: config.INTEGRATION_ENCRYPTION_KEY_VERSION,
keys: Object.freeze({
...config.INTEGRATION_ENCRYPTION_OLD_KEYS,
[config.INTEGRATION_ENCRYPTION_KEY_VERSION]:
config.INTEGRATION_ENCRYPTION_KEY,
}),
}
}
function envelope(value: {
readonly keyVersion: string
readonly nonce: Uint8Array
readonly ciphertext: Uint8Array
readonly authTag: Uint8Array
}): SecretEnvelopeV1 {
return {
algorithm: 'AES-256-GCM',
envelopeVersion: 1,
keyVersion: value.keyVersion,
nonce: Buffer.from(value.nonce).toString('base64'),
ciphertext: Buffer.from(value.ciphertext).toString('base64'),
authenticationTag: Buffer.from(value.authTag).toString('base64'),
}
}
export function createGiteaSnapshotDependencies(
config: Configuration,
): RepositorySnapshotDependencies {
const integrations = new DrizzleGiteaIntegrationStore()
const persistence = new RepositorySnapshotStore()
return {
persistence,
async createReader({ workspaceId, integrationId }) {
const stored = await integrations.findWithSecretForWorkspace(
workspaceId,
integrationId,
)
if (!stored || stored.integration.status === 'disabled') {
throw new Error('Gitea snapshot integration is unavailable')
}
const token = decryptIntegrationSecret(
envelope(stored.secret),
{
workspaceId,
integrationId,
secretKind: 'access-token',
},
keyRing(config),
)
const policy: GiteaNetworkPolicy = {
privateNetworkPolicy: config.GITEA_PRIVATE_NETWORK_POLICY,
allowedHosts: allowedHosts(config.GITEA_ALLOWED_HOSTS),
allowInsecureHttp: stored.allowPrivateHttp,
requestTimeoutMs: stored.requestTimeoutMs,
maxResponseBytes: config.MAX_EVIDENCE_BYTES,
maxRedirects: config.GITEA_MAX_REDIRECTS,
}
return new GiteaAdapter(
new GiteaClient({
baseUrl: stored.integration.baseUrl,
token,
networkPolicy: policy,
}),
)
},
}
}
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest'
import type { JobRecord } from '@devrunbook/application'
import { createJobHandlers } from './handlers'
function job(payload: JobRecord['payload']): JobRecord {
const timestamp = new Date('2026-07-27T12:00:00.000Z')
return {
id: '00000000-0000-4000-8000-000000000911',
workspaceId: null,
type: 'system.health-probe',
state: 'running',
idempotencyKey: 'probe-1',
payload,
progress: {},
attemptCount: 1,
maxAttempts: 3,
leaseOwner: 'worker:lease',
leaseExpiresAt: timestamp,
availableAt: timestamp,
startedAt: timestamp,
finishedAt: null,
errorCode: null,
errorDetailRedacted: null,
createdAt: timestamp,
updatedAt: timestamp,
}
}
describe('worker job handlers', () => {
it('handles a durable health probe as data without executing payload text', async () => {
const handler = createJobHandlers(
() => new Date('2026-07-27T12:01:00.000Z'),
)['system.health-probe']
if (!handler) throw new Error('Health probe handler is not registered')
await expect(
handler(job({ requestedBy: '$(unsafe command)' }), {
signal: new AbortController().signal,
heartbeat: async () => true,
}),
).resolves.toEqual({
status: 'ok',
checkedAt: '2026-07-27T12:01:00.000Z',
workerProtocol: 1,
})
})
it('rejects unsupported payload fields permanently', async () => {
const handler = createJobHandlers()['system.health-probe']
if (!handler) throw new Error('Health probe handler is not registered')
await expect(
handler(job({ command: 'whoami' }), {
signal: new AbortController().signal,
heartbeat: async () => true,
}),
).rejects.toMatchObject({ code: 'health_probe_payload_invalid' })
})
it('registers repository collection only when explicit dependencies are supplied', () => {
expect(createJobHandlers()['gitea.repository-snapshot']).toBeUndefined()
expect(
createJobHandlers(undefined, {
createReader: async () => {
throw new Error('not used')
},
persistence: {
resolveCollectionTarget: async () => null,
completeCollection: async () => null,
failCollection: async () => false,
},
})['gitea.repository-snapshot'],
).toBeTypeOf('function')
})
})
+67
View File
@@ -0,0 +1,67 @@
import {
PermanentJobError,
type JobHandlers,
type JobJsonValue,
} from '@devrunbook/application'
import {
createRepositorySnapshotJobHandler,
type RepositorySnapshotDependencies,
} from './repository-snapshot'
function validateHealthProbePayload(payload: JobJsonValue): void {
if (
payload === null ||
Array.isArray(payload) ||
typeof payload !== 'object'
) {
throw new PermanentJobError(
'health_probe_payload_invalid',
'Health probe payload must be a JSON object',
)
}
const objectPayload = payload as { readonly [key: string]: JobJsonValue }
const keys = Object.keys(objectPayload)
if (keys.some((key) => key !== 'requestedBy')) {
throw new PermanentJobError(
'health_probe_payload_invalid',
'Health probe payload contains unsupported fields',
)
}
const requestedBy = objectPayload.requestedBy
if (
requestedBy !== undefined &&
(typeof requestedBy !== 'string' || requestedBy.length > 100)
) {
throw new PermanentJobError(
'health_probe_payload_invalid',
'Health probe requestedBy must be a string of at most 100 characters',
)
}
}
/**
* The baseline probe proves durable worker dispatch without evaluating payload
* text or invoking a shell. Product job handlers are added explicitly here.
*/
export function createJobHandlers(
now: () => Date = () => new Date(),
repositorySnapshot?: RepositorySnapshotDependencies,
): JobHandlers {
const handlers: JobHandlers = {
'system.health-probe': async (job) => {
validateHealthProbePayload(job.payload)
return {
status: 'ok',
checkedAt: now().toISOString(),
workerProtocol: 1,
}
},
...(repositorySnapshot
? {
'gitea.repository-snapshot':
createRepositorySnapshotJobHandler(repositorySnapshot),
}
: {}),
}
return Object.freeze(handlers)
}
@@ -0,0 +1,443 @@
import { describe, expect, it, vi } from 'vitest'
import type { JobRecord } from '@devrunbook/application'
import {
createRepositorySnapshotJobHandler,
RepositorySnapshotSourceError,
type RepositorySnapshotDependencies,
type RepositorySnapshotPersistence,
type RepositorySnapshotReader,
type SnapshotCapability,
} from './repository-snapshot'
const ids = {
job: '00000000-0000-4000-8000-000000000901',
workspace: '00000000-0000-4000-8000-000000000902',
repository: '00000000-0000-4000-8000-000000000903',
integration: '00000000-0000-4000-8000-000000000904',
snapshot: '00000000-0000-4000-8000-000000000905',
user: '00000000-0000-4000-8000-000000000906',
revision: '00000000-0000-4000-8000-000000000907',
}
function job(overrides: Partial<JobRecord> = {}): JobRecord {
const timestamp = new Date('2026-07-27T12:00:00.000Z')
return {
id: ids.job,
workspaceId: ids.workspace,
type: 'gitea.repository-snapshot',
state: 'running',
idempotencyKey: 'repository-snapshot-1',
payload: {
schemaVersion: 1,
workspaceId: ids.workspace,
repositoryId: ids.repository,
integrationId: ids.integration,
requestedBy: ids.user,
collectionMode: 'bounded-read-only',
profileRevisionPolicy: 'create-initial-only',
},
progress: {},
attemptCount: 1,
maxAttempts: 3,
leaseOwner: 'worker:lease',
leaseExpiresAt: timestamp,
availableAt: timestamp,
startedAt: timestamp,
finishedAt: null,
errorCode: null,
errorDetailRedacted: null,
createdAt: timestamp,
updatedAt: timestamp,
...overrides,
}
}
const supported: SnapshotCapability = {
status: 'supported',
checkedAt: '2026-07-27T12:00:00.000Z',
}
function reader(
overrides: Partial<RepositorySnapshotReader> = {},
): RepositorySnapshotReader {
const files: Record<string, string> = {
'package.json': JSON.stringify({
scripts: { test: '$(unsafe-command)', build: 'node build.js' },
dependencies: { next: '15.0.0', react: '19.0.0' },
}),
'pnpm-lock.yaml': 'lockfileVersion: 9',
'README.md': 'setup text that is untrusted and never executed',
}
return {
getRepository: vi.fn(async () => ({
id: '42',
owner: 'devrunbook',
name: 'platform',
fullName: 'devrunbook/platform',
defaultBranch: 'main',
archived: false,
private: true,
})),
getCapabilities: vi.fn(async () => ({
branches: supported,
tags: supported,
releases: supported,
contents: supported,
'branch-protection': supported,
workflows: supported,
})),
listTree: vi.fn<RepositorySnapshotReader['listTree']>(
async (_repository, _ref, page = 1) =>
page === 1
? [
{
path: 'src',
kind: 'directory' as const,
sha: 'd1',
size: null,
},
{
path: 'tests',
kind: 'directory' as const,
sha: 'd2',
size: null,
},
...Object.entries(files).map(([path, value]) => ({
path,
kind: 'file' as const,
sha: `sha-${path}`,
size: Buffer.byteLength(value),
})),
]
: [],
),
getFile: vi.fn(async (_repository, _ref, path) => {
const bytes = Buffer.from(files[path]!, 'utf8')
return { path, sha: `sha-${path}`, size: bytes.byteLength, bytes }
}),
getBranches: vi.fn(async () => [
{ name: 'main', commitSha: 'abc123', protected: true },
]),
getTags: vi.fn(async () => ['v2', 'v1']),
getReleases: vi.fn(async () => ['v1']),
getGovernanceEvidence: vi.fn(async () => ({ protected: true })),
getWorkflowEvidence: vi.fn(async () => ({ workflows: 1 })),
...overrides,
}
}
function dependencies(
source = reader(),
overrides: Partial<RepositorySnapshotDependencies> = {},
) {
const completeCollection = vi.fn<
RepositorySnapshotPersistence['completeCollection']
>(async () => ({
snapshotId: ids.snapshot,
profileRevision: {
id: ids.revision,
revisionNumber: 1,
contentDigest: 'a'.repeat(64),
},
findingCount: 1,
}))
const failCollection = vi.fn<RepositorySnapshotPersistence['failCollection']>(
async () => true,
)
const resolveCollectionTarget = vi.fn<
RepositorySnapshotPersistence['resolveCollectionTarget']
>(async () => ({
snapshotId: ids.snapshot,
owner: 'devrunbook',
name: 'platform',
}))
return {
dependencies: {
createReader: vi.fn(async () => source),
persistence: {
resolveCollectionTarget,
completeCollection,
failCollection,
},
now: () => new Date('2026-07-27T12:05:00.000Z'),
...overrides,
} satisfies RepositorySnapshotDependencies,
completeCollection,
failCollection,
}
}
const context = {
signal: new AbortController().signal,
heartbeat: vi.fn(async () => true),
}
describe('repository snapshot job handler', () => {
it('retries when queue dispatch races the snapshot target binding', async () => {
const fixture = dependencies()
fixture.dependencies.persistence.resolveCollectionTarget = async () => null
const handler = createRepositorySnapshotJobHandler(fixture.dependencies)
await expect(handler(job(), context)).rejects.toMatchObject({
code: 'repository_snapshot_target_pending',
})
})
it('persists deterministic bounded evidence and never adopts manifest script text', async () => {
const source = reader()
const fixture = dependencies(source)
const handler = createRepositorySnapshotJobHandler(fixture.dependencies)
await expect(handler(job(), context)).resolves.toEqual({
status: 'complete',
snapshotId: ids.snapshot,
findingCount: 1,
profileRevisionId: ids.revision,
inspectedFiles: 3,
inspectedBytes: 179,
})
expect(fixture.completeCollection).toHaveBeenCalledOnce()
const request = fixture.completeCollection.mock.calls[0]![0]
const evidence = request.evidence as {
collectionStatus: string
files: readonly unknown[]
proposedProfile: unknown
}
expect(request.profileRevisionPolicy).toBe('create-initial-only')
expect(evidence.collectionStatus).toBe('complete')
expect(request.evidence).not.toHaveProperty('rawContent')
expect(evidence.files).toEqual([
expect.objectContaining({ path: 'package.json' }),
expect.objectContaining({ path: 'pnpm-lock.yaml' }),
expect.objectContaining({ path: 'README.md' }),
])
expect(evidence.proposedProfile).toEqual(request.profile)
const profile = request.profile as {
spec: {
commands: readonly {
command: string
confirmed: boolean
safeForAgentSuggestion: boolean
}[]
}
}
expect(profile.spec.commands).toEqual(
expect.arrayContaining([
expect.objectContaining({
command: 'pnpm test',
confirmed: false,
safeForAgentSuggestion: false,
}),
]),
)
expect(JSON.stringify(request)).not.toContain('$(unsafe-command)')
expect(source.getFile).toHaveBeenCalledTimes(3)
})
it('skips file analysis and cancels collection when preflight evidence is unchanged', async () => {
const source = reader()
const fixture = dependencies(source)
const cancelUnchangedCollection = vi.fn(async () => true)
const baseline = dependencies(source)
await createRepositorySnapshotJobHandler(baseline.dependencies)(
job(),
context,
)
const evidence = baseline.completeCollection.mock.calls[0]![0].evidence as {
preflightDigest: string
}
const treeCallsBeforePreflight = vi.mocked(source.listTree).mock.calls
.length
fixture.dependencies.persistence.getLastPreflightDigest = async () =>
evidence.preflightDigest
fixture.dependencies.persistence.cancelUnchangedCollection =
cancelUnchangedCollection
const result = await createRepositorySnapshotJobHandler(
fixture.dependencies,
)(job(), context)
expect(result).toMatchObject({ status: 'unchanged', inspectedFiles: 0 })
expect(cancelUnchangedCollection).toHaveBeenCalledOnce()
expect(source.listTree).toHaveBeenCalledTimes(treeCallsBeforePreflight)
expect(fixture.completeCollection).not.toHaveBeenCalled()
})
it('completes explicitly partial and accepts no automatic profile revision', async () => {
const source = reader({
getCapabilities: vi.fn<RepositorySnapshotReader['getCapabilities']>(
async () => ({
contents: supported,
branches: {
status: 'forbidden',
checkedAt: '2026-07-27T12:00:00.000Z',
errorCode: 'PERMISSION_MISSING',
},
tags: supported,
releases: {
status: 'unsupported',
checkedAt: '2026-07-27T12:00:00.000Z',
},
'branch-protection': {
status: 'forbidden',
checkedAt: '2026-07-27T12:00:00.000Z',
},
workflows: supported,
}),
),
})
const fixture = dependencies(source)
fixture.completeCollection.mockResolvedValueOnce({
snapshotId: ids.snapshot,
profileRevision: null,
findingCount: 2,
})
await expect(
createRepositorySnapshotJobHandler(fixture.dependencies)(job(), context),
).resolves.toMatchObject({ status: 'partial', profileRevisionId: null })
const request = fixture.completeCollection.mock.calls[0]![0]
const evidence = request.evidence as { limitations: readonly string[] }
expect(evidence.limitations).toEqual([
'branch-protection',
'branches',
'releases',
])
expect(request.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({ ruleId: 'REPO_SNAPSHOT_PARTIAL' }),
]),
)
expect(request.findings).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ ruleId: 'REPO_NO_RELEASE_HISTORY' }),
]),
)
})
it('skips sensitive paths and enforces both file-count and byte limits', async () => {
const content = '12345678'
const source = reader({
listTree: vi.fn<RepositorySnapshotReader['listTree']>(
async (_repository, _ref, page = 1) =>
page === 1
? [
{ path: '.env', kind: 'file' as const, sha: 'a', size: 1 },
{
path: 'private.pem',
kind: 'file' as const,
sha: 'b',
size: 1,
},
{ path: 'README.md', kind: 'file' as const, sha: 'c', size: 8 },
{
path: 'package.json',
kind: 'file' as const,
sha: 'd',
size: 8,
},
]
: [],
),
getFile: vi.fn(async (_repository, _ref, path) => ({
path,
sha: path,
size: 8,
bytes: Buffer.from(content),
})),
})
const fixture = dependencies(source, {
limits: {
maximumFiles: 2,
maximumFileBytes: 8,
maximumTotalBytes: 8,
maximumTreePages: 2,
},
})
await createRepositorySnapshotJobHandler(fixture.dependencies)(
job(),
context,
)
expect(source.getFile).toHaveBeenCalledOnce()
expect(source.getFile).toHaveBeenCalledWith(
{ owner: 'devrunbook', name: 'platform' },
'main',
'package.json',
8,
)
const request = fixture.completeCollection.mock.calls[0]![0]
const evidence = request.evidence as { limitations: readonly string[] }
expect(evidence.limitations).toEqual([
'inspected-byte-limit',
'inspected-file-count-limit',
])
expect(JSON.stringify(request)).not.toContain('.env')
expect(JSON.stringify(request)).not.toContain('private.pem')
})
it('retries safe transient source failures without prematurely failing the snapshot', async () => {
const fixture = dependencies(reader(), {
createReader: vi.fn(async () => {
throw new RepositorySnapshotSourceError('RATE_LIMITED', true)
}),
})
await expect(
createRepositorySnapshotJobHandler(fixture.dependencies)(job(), context),
).rejects.toMatchObject({
name: 'TransientJobError',
code: 'repository_snapshot_rate_limited',
})
expect(fixture.failCollection).not.toHaveBeenCalled()
})
it('records only a safe code when the final source attempt fails', async () => {
const fixture = dependencies(reader(), {
createReader: vi.fn(async () => {
throw new RepositorySnapshotSourceError('AUTH_INVALID', false)
}),
})
await expect(
createRepositorySnapshotJobHandler(fixture.dependencies)(
job({ attemptCount: 3 }),
context,
),
).rejects.toMatchObject({
name: 'PermanentJobError',
code: 'repository_snapshot_auth_invalid',
})
expect(fixture.failCollection).toHaveBeenCalledWith({
workspaceId: ids.workspace,
snapshotId: ids.snapshot,
safeCode: 'AUTH_INVALID',
})
})
it('rejects command-like and extra payload data before resolving a reader', async () => {
const fixture = dependencies()
await expect(
createRepositorySnapshotJobHandler(fixture.dependencies)(
job({
payload: {
schemaVersion: 1,
workspaceId: ids.workspace,
repositoryId: ids.repository,
integrationId: ids.integration,
requestedBy: ids.user,
collectionMode: 'bounded-read-only',
profileRevisionPolicy: 'create-initial-only',
command: 'whoami',
},
}),
context,
),
).rejects.toMatchObject({
code: 'repository_snapshot_payload_invalid',
})
expect(fixture.dependencies.createReader).not.toHaveBeenCalled()
})
})
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, it, vi } from 'vitest'
import type { JobStore } from '@devrunbook/application'
import { runWorkerLoop } from './worker-loop'
describe('worker loop', () => {
it('stops cleanly after an idle poll is aborted', async () => {
const controller = new AbortController()
const store = {
claim: vi.fn(async () => {
controller.abort()
return null
}),
} as unknown as JobStore
await runWorkerLoop({
store,
handlers: {},
workerId: 'worker-test',
nextLeaseId: () => 'lease-test',
leaseDurationMs: 30_000,
pollIntervalMs: 2_000,
signal: controller.signal,
})
expect(store.claim).toHaveBeenCalledOnce()
})
it('runs periodic planning before polling and contains planner failures', async () => {
const controller = new AbortController()
const order: string[] = []
const store = {
claim: vi.fn(async () => {
order.push('poll')
controller.abort()
return null
}),
} as unknown as JobStore
await runWorkerLoop({
store,
handlers: {},
workerId: 'worker-test',
nextLeaseId: () => 'lease-test',
leaseDurationMs: 30_000,
pollIntervalMs: 2_000,
signal: controller.signal,
schedule: async () => {
order.push('schedule')
throw new Error('temporary planner failure')
},
onScheduleError: vi.fn(),
})
expect(order).toEqual(['schedule', 'poll'])
})
})
+69
View File
@@ -0,0 +1,69 @@
import {
processNextJob,
type JobHandlers,
type JobStore,
type ProcessJobResult,
} from '@devrunbook/application'
export interface WorkerLoopOptions {
readonly store: JobStore
readonly handlers: JobHandlers
readonly workerId: string
readonly nextLeaseId: () => string
readonly leaseDurationMs: number
readonly pollIntervalMs: number
readonly signal: AbortSignal
readonly onResult?: (result: ProcessJobResult) => void
readonly onPollError?: (error: unknown) => void
readonly schedule?: () => Promise<void>
readonly scheduleIntervalMs?: number
readonly onScheduleError?: (error: unknown) => void
readonly now?: () => number
}
function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
if (signal.aborted) return Promise.resolve()
return new Promise((resolve) => {
const timer = setTimeout(done, milliseconds)
timer.unref()
signal.addEventListener('abort', done, { once: true })
function done() {
clearTimeout(timer)
signal.removeEventListener('abort', done)
resolve()
}
})
}
/** Sequential polling prevents one worker process from over-claiming jobs. */
export async function runWorkerLoop(options: WorkerLoopOptions): Promise<void> {
const now = options.now ?? Date.now
let nextScheduleAt = 0
while (!options.signal.aborted) {
if (options.schedule && now() >= nextScheduleAt) {
nextScheduleAt = now() + (options.scheduleIntervalMs ?? 300_000)
try {
await options.schedule()
} catch (error) {
options.onScheduleError?.(error)
}
}
try {
const result = await processNextJob({
store: options.store,
handlers: options.handlers,
workerId: options.workerId,
nextLeaseId: options.nextLeaseId,
leaseDurationMs: options.leaseDurationMs,
})
options.onResult?.(result)
if (result.outcome === 'idle') {
await wait(options.pollIntervalMs, options.signal)
}
} catch (error) {
options.onPollError?.(error)
await wait(options.pollIntervalMs, options.signal)
}
}
}
@@ -0,0 +1,38 @@
import { pathToFileURL } from 'node:url'
import { enforceArtifactRetention } from '@devrunbook/application'
import { LocalArtifactStorage } from '@devrunbook/artifacts'
import { parseEnvironment } from '@devrunbook/config'
import { closeDatabase, DrizzleArtifactRetentionStore } from '@devrunbook/db'
export async function runArtifactRetention(
environment: Record<string, string | undefined> = process.env,
) {
const config = parseEnvironment(environment)
const store = new DrizzleArtifactRetentionStore()
const storage = new LocalArtifactStorage(config.ARTIFACT_ROOT)
const totals = { scanned: 0, deleted: 0, missing: 0 }
while (true) {
const result = await enforceArtifactRetention({
store,
storage,
limit: 500,
})
totals.scanned += result.scanned
totals.deleted += result.deleted
totals.missing += result.missing
if (result.scanned < 500) return Object.freeze(totals)
}
}
const entryPoint = process.argv[1]
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
try {
const result = await runArtifactRetention()
process.stdout.write(
`${JSON.stringify({ outcome: 'success', ...result })}\n`,
)
} finally {
await closeDatabase()
}
}
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from 'vitest'
import { runOperatorPasswordReset } from './password-reset'
const environment = {
SESSION_SECRET: 's'.repeat(32),
PUBLIC_BASE_URL: 'https://runbook.example.test',
}
describe('operator password reset command', () => {
it('accepts only an email and emits exactly one reset URL line', async () => {
const output = vi.fn()
const issue = vi.fn(async () => ({
resetUrl:
'https://runbook.example.test/reset-password#token=secret-token',
expiresAt: new Date('2026-07-27T12:30:00Z'),
}))
await runOperatorPasswordReset(
['owner@example.test'],
environment,
output,
issue,
)
expect(issue).toHaveBeenCalledWith({
email: 'owner@example.test',
publicBaseUrl: 'https://runbook.example.test',
})
expect(output).toHaveBeenCalledOnce()
expect(output).toHaveBeenCalledWith(
'https://runbook.example.test/reset-password#token=secret-token',
)
})
it('rejects a second argument instead of accepting a new password', async () => {
const issue = vi.fn()
await expect(
runOperatorPasswordReset(
['owner@example.test', 'new-password'],
environment,
vi.fn(),
issue,
),
).rejects.toThrow('Usage:')
expect(issue).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,72 @@
import { pathToFileURL } from 'node:url'
import {
issueOperatorPasswordResetToken,
TokenDigester,
type IssueOperatorPasswordResetRequest,
type IssuedPasswordReset,
} from '@devrunbook/application'
import { closeDatabase, DrizzlePasswordResetStore } from '@devrunbook/db'
type IssueReset = (
request: IssueOperatorPasswordResetRequest,
) => Promise<IssuedPasswordReset>
export async function runOperatorPasswordReset(
arguments_: readonly string[],
environment: Record<string, string | undefined>,
writeOutput: (value: string) => void,
issueReset?: IssueReset,
): Promise<void> {
if (arguments_.length !== 1 || !arguments_[0]) {
throw new Error(
'Usage: pnpm --filter @devrunbook/worker operator:password-reset -- <user-email>',
)
}
const sessionSecret = environment.SESSION_SECRET
const publicBaseUrl = environment.PUBLIC_BASE_URL
if (!sessionSecret || sessionSecret.length < 32) {
throw new Error('SESSION_SECRET must contain at least 32 characters')
}
if (!publicBaseUrl) throw new Error('PUBLIC_BASE_URL is required')
const issue =
issueReset ??
((request) =>
issueOperatorPasswordResetToken(
{
store: new DrizzlePasswordResetStore(),
digester: new TokenDigester(Buffer.from(sessionSecret, 'utf8')),
},
request,
))
const result = await issue({
email: arguments_[0],
publicBaseUrl,
})
// Successful stdout is deliberately one line containing only the reset URL.
writeOutput(result.resetUrl)
}
async function main() {
try {
await runOperatorPasswordReset(
process.argv.slice(2),
process.env,
(value) => console.log(value),
)
} catch (error) {
const message =
error instanceof Error ? error.message : 'Password reset failed'
console.error(message)
process.exitCode = 1
} finally {
await closeDatabase()
}
}
const entryPoint = process.argv[1]
if (entryPoint && import.meta.url === pathToFileURL(entryPoint).href) {
await main()
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "outDir": "dist", "rootDir": "src", "types": ["node"] },
"include": ["src/**/*.ts"]
}