Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9

This commit is contained in:
Public Source Publisher committed 2026-10-03 17:07:21 +00:00
1 parent 10f2edcd29
commit 59f15b2d08
218 files changed
+23995 -1849

No files matched your search

+7
View File
@@ -186,6 +186,13 @@ At minimum isolate policies for:
Return a generic response that does not confirm account/content existence.
The knowledge and unified-search endpoints share a 60-request/minute budget per
effective remote IP. Job creation and the compatibility backup-creation endpoint
share a 10-request/minute budget. Both use zero-length queues and return 429.
Forwarded addresses are accepted only from configured trusted proxies. Durable
job admission additionally enforces per-actor, workspace and rolling daily budgets
inside PostgreSQL; see [background jobs](background-jobs.md).
## Security verification
Release requires:
+75 -12
View File
@@ -16,7 +16,8 @@ A full backup set contains:
5. Product, schema and container/image versions.
6. File inventory with sizes and SHA-256 hashes.
7. Backup job metadata and timestamps.
8. Encryption metadata when operator-managed encryption is enabled.
8. A signed integrity receipt authenticating the exact manifest.
9. Encryption metadata when operator-managed encryption is enabled.
Do not include runtime session secrets or plaintext API tokens.
@@ -42,13 +43,46 @@ mutable files. Deletions use retention, preventing a blob from disappearing duri
5. copy/link/archive required blobs without following unsafe paths;
6. create manifest;
7. verify dump readability and every file hash;
8. optionally encrypt with an operator-supplied mechanism;
9. atomically publish completed backup;
10. apply retention only after a verified new backup exists;
11. record audit and administrator-visible result.
8. authenticate the manifest hash with HMAC-SHA-256;
9. optionally encrypt with an operator-supplied mechanism;
10. atomically publish completed backup;
11. apply retention only after a verified new backup exists;
12. record audit and administrator-visible result.
A failed backup never replaces the last known-good backup.
New backup directories and files use private Unix permissions (0700 and 0600).
Permission failures stop creation before publication. On Windows, provision and
verify restrictive NTFS ACLs for backups, keys and restore targets before use;
Unix mode checks do not enforce Windows ACLs. HMAC authenticates backups but
does not encrypt their contents, so archive/share access must remain restricted.
## Backup authenticity key and recovery
The `.integrity.json` receipt beside a backup is not trusted by itself. The application
authenticates its backup ID, exact manifest hash, completion time and verification result
with HMAC-SHA-256. The random 32-byte key is stored at
`KnowledgeHub:Paths:Keys/backup-integrity.key`; in the supplied Compose deployment this
is `/var/lib/knowledge-hub/keys/backup-integrity.key`. This location is outside
`KnowledgeHub:Paths:Backups`, so someone who can rewrite a backup and its receipt cannot
create a valid signature. The application creates the key when integrity evidence is
first written and restricts it to the service account on Unix hosts.
Treat this key as essential recovery material. Copy the exact key file into an encrypted,
access-controlled escrow location separate from the backup storage, and include it in
the operator's disaster-recovery procedure. To restore on a replacement host, recover
the key to the configured Keys directory before verifying or restoring backups. Never
put the key in a backup directory or in the same backup archive as the signed receipts.
Protect access to the escrow copy as carefully as access to the backups.
If the key is missing or unreadable, backup integrity is reported as unknown and restore
is refused. Do not replace a lost key with a newly generated one and expect existing
backups to validate: a new key authenticates only backups created after it. Recover the
original key from escrow first. If no copy exists, the existing backups cannot be
cryptographically authenticated by the application and must not be treated as verified.
Backups made before authenticated receipts were introduced also remain unverified; create
a new backup with this version before relying on application-verified restore.
## Retention defaults
Configurable starting policy:
@@ -65,13 +99,26 @@ source.
Restore is deliberately separate from normal application startup.
1. Select backup.
2. Verify manifest, hashes, version compatibility and encryption.
3. Create clean PostgreSQL database and empty target directories.
4. Restore database.
1. Select backup. Copy read-only media to a writable recovery directory first,
preserving the manifest, signed receipts and file bytes. Recover the exact
`backup-integrity.key` from protected external escrow into the configured Keys
directory; never take it from the backup archive. The application acquires an
exclusive storage lease there to prevent retention from removing a restore source,
and writes its restore receipt there. Keep the original media unchanged.
2. Use the backup's recorded application image to create a clean PostgreSQL
database at exactly the manifest's schema version. Create an existing, empty
target attachments directory on writable storage with room for the full payload.
3. Copy the manifest and every listed database and attachment file into a private
staging directory inside the target attachments volume. Verify the HMAC against
the staged manifest bytes and verify each staged file's size and SHA-256 before
opening or changing the target database. Restore reads only from this verified
snapshot, so source-volume changes after verification cannot change restored bytes.
4. Restore database. The restore service refuses a target whose latest applied
migration differs from the authenticated manifest, before inserting any rows.
5. Restore attachment blobs by hash.
6. Start the recorded/current compatible application in maintenance mode.
7. Apply supported migrations.
6. Verify the restored data with the recorded application version.
7. Upgrade through supported migrations using the intended newer image, then
repeat integrity checks before promotion.
8. Run integrity checks:
- FK/schema;
- user/membership;
@@ -87,6 +134,21 @@ Restore is deliberately separate from normal application startup.
Never test restore by overwriting the only production instance.
Pass target PostgreSQL credentials through `--target-connection-file=<path>`, which
reads the connection string from a protected file. On Unix, the file must be readable
only by the restore account (mode `0600` or `0400`); the restore command rejects files
with group or other access. On Windows, apply an ACL that grants access only to the
restore operator/service account. Do not put the connection string in command-line
arguments. Restore errors deliberately omit provider exception details because they
can contain connection data.
The real-backup drill also requires the exact integrity key recovered from protected
external escrow. Pass only its file path through
`KNOWLEDGE_HUB_BACKUP_INTEGRITY_KEY_FILE`; the script copies the key with mode `0600`
into its temporary working directory, uses that directory as the restore Keys path,
and removes the temporary copy during cleanup. The key contents are never placed in an
argument or printed by the script.
## Backup encryption
The application may integrate an operator-supplied encryption command or library after
@@ -125,11 +187,12 @@ isolated restore proves it is necessary and the operator has assessed newer writ
- create backup with representative data;
- verify manifest;
- reject a forged manifest and matching unsigned/invalid integrity receipt;
- corrupt dump and blob tests;
- clean-target restore;
- compare logical counts and hashes;
- restore older supported schema then migrate;
- missing key/encrypted backup failure;
- missing integrity key or encryption key failure;
- interrupted backup cleanup;
- retention policy;
- no secret inclusion;
+454 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: ItWorx Knowledge Hub API
version: 0.1.0-draft
version: 1.0.0-rc.1
description: |
Versioned API for the ItWorx Knowledge Hub web client and scoped automation.
This is not an MCP transport. The future MCP service will consume this API.
@@ -29,6 +29,9 @@ tags:
- name: Activity
- name: Workspace
- name: Ingestion proposals
- name: Preferences
- name: DataSources
- name: Jobs
security:
- cookieAuth: []
- bearerAuth: []
@@ -777,6 +780,59 @@ paths:
$ref: "#/components/schemas/SearchResponse"
"401":
$ref: "#/components/responses/Unauthorized"
/unified-search:
get:
tags: [Search]
operationId: searchWorkspaceContent
description: >-
Human reader session only. Searches visible knowledge, projects, reviewed
services, runbooks and decisions in the current workspace. Trashed content
and hidden or archived services are excluded. Counts cover all five kinds;
`kind` filters the returned page. A `tag` filters exact, case-insensitive
page tags on knowledge, runbook and decision results and excludes projects
and services. An empty query returns no results unless `tag` is supplied. The
endpoint shares the search rate limit of 60 requests per effective remote IP
per minute. An invalid `kind` returns 422.
parameters:
- name: q
in: query
schema:
type: string
maxLength: 500
- name: kind
in: query
schema:
type: string
enum: [knowledge, projects, services, runbooks, decisions]
- name: tag
in: query
description: Exact, case-insensitive page tag; applies to knowledge, runbooks and decisions.
schema:
type: string
maxLength: 64
- $ref: "#/components/parameters/Cursor"
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 25
responses:
"200":
description: Permission-filtered, ranked results and complete grouped counts
content:
application/json:
schema:
$ref: "#/components/schemas/UnifiedSearchResponse"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"422":
description: Invalid kind
"429":
description: Search request budget exhausted
/rag/publication-manifest:
get:
tags: [Rag]
@@ -3077,6 +3133,180 @@ paths:
application/problem+json:
schema:
$ref: "#/components/schemas/ProblemDetails"
/me/preferences:
get:
tags: [Preferences]
operationId: getMyPreferences
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
put:
tags: [Preferences]
operationId: updateMyPreferences
security: [{ cookieAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferencesRequest" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/UserPreferences" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"412": { $ref: "#/components/responses/PreconditionFailed" }
"428": { $ref: "#/components/responses/PreconditionRequired" }
/admin/platform-settings:
get:
tags: [Admin]
operationId: getPlatformSettings
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
put:
tags: [Admin]
operationId: updatePlatformSettings
security: [{ cookieAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettingsRequest" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/PlatformSettings" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"412": { $ref: "#/components/responses/PreconditionFailed" }
"428": { $ref: "#/components/responses/PreconditionRequired" }
/admin/data-sources:
get:
tags: [DataSources]
operationId: listDataSources
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceList" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [DataSources]
operationId: registerDataSource
security: [{ cookieAuth: [] }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceRegistration" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/admin/data-sources/{id}:
put:
tags: [DataSources]
operationId: configureDataSource
security: [{ cookieAuth: [] }]
parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceConfiguration" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"412": { $ref: "#/components/responses/PreconditionFailed" }
"428": { $ref: "#/components/responses/PreconditionRequired" }
/admin/data-sources/{id}/observations:
post:
tags: [DataSources]
operationId: reportDataSourceObservation
security: [{ cookieAuth: [] }]
parameters: [{ name: id, in: path, required: true, schema: { type: string, format: uuid } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/DataSourceObservation" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/DataSource" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/admin/backups/status:
get:
tags: [Backups]
operationId: getBackupStatus
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupStatus" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/admin/backups/{backupId}/protection:
put:
tags: [Backups]
operationId: setBackupProtection
security: [{ cookieAuth: [] }]
parameters: [{ name: backupId, in: path, required: true, schema: { type: string } }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/BackupProtectionRequest" } } } }
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackupEvidence" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"412": { $ref: "#/components/responses/PreconditionFailed" }
"428": { $ref: "#/components/responses/PreconditionRequired" }
/admin/audit-log:
get:
tags: [Admin]
operationId: listAdminAuditLog
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/AdminAuditList" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs:
get:
tags: [Jobs]
operationId: listBackgroundJobs
security: [{ cookieAuth: [] }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJobList" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs/course-tagging:
post:
tags: [Jobs]
operationId: queueCourseTaggingJob
security: [{ cookieAuth: [] }]
parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueCourseTaggingJobRequest" } } } }
responses:
"202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs/backups:
post:
tags: [Jobs]
operationId: queueBackupJob
security: [{ cookieAuth: [] }]
parameters: [{ $ref: "#/components/parameters/RequiredIdempotencyKey" }]
requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/QueueBackupJobRequest" } } } }
responses:
"202": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs/{jobId}:
get:
tags: [Jobs]
operationId: getBackgroundJob
security: [{ cookieAuth: [] }]
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs/{jobId}/cancel:
post:
tags: [Jobs]
operationId: cancelBackgroundJob
security: [{ cookieAuth: [] }]
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: Persisted result, content: { application/json: { schema: { $ref: "#/components/schemas/BackgroundJob" } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/jobs/{jobId}/events:
get:
tags: [Jobs]
operationId: streamBackgroundJobEvents
security: [{ cookieAuth: [] }]
parameters: [{ name: jobId, in: path, required: true, schema: { type: string, format: uuid } }]
responses:
"200": { description: Job and progress Server-Sent Events with heartbeat and terminal close, content: { text/event-stream: { schema: { type: string } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
components:
securitySchemes:
cookieAuth:
@@ -3248,6 +3478,194 @@ components:
schema:
$ref: "#/components/schemas/ProblemDetails"
schemas:
UserPreferencesRequest:
type: object
required: [theme, density, primaryNavigationCollapsed, contextPanelOpen, navigationOrder, dashboardWidgetOrder, hiddenDashboardWidgets, defaultLandingPage, catalogView, contextPanelWidth, navigationWidth]
properties:
theme: { type: string, enum: [system, light, dark] }
density: { type: string, enum: [comfortable, compact] }
primaryNavigationCollapsed: { type: boolean }
contextPanelOpen: { type: boolean }
navigationOrder: { type: array, items: { type: string } }
dashboardWidgetOrder: { type: array, items: { type: string } }
hiddenDashboardWidgets: { type: array, items: { type: string } }
defaultLandingPage: { type: string }
catalogView: { type: string, enum: [grid, list] }
contextPanelWidth: { type: integer, minimum: 256, maximum: 440 }
navigationWidth: { type: integer, minimum: 192, maximum: 320 }
UserPreferences:
allOf:
- $ref: "#/components/schemas/UserPreferencesRequest"
- type: object
required: [schemaVersion, eTag, isDefault, updatedAt]
properties:
schemaVersion: { type: integer, enum: [1] }
eTag: { type: string }
isDefault: { type: boolean }
updatedAt: { type: [string, "null"], format: date-time }
PlatformSettingsRequest:
type: object
required: [displayName, primaryLocale, timeZone, ingestionProposals, backupPolicyExpectation, inventoryFreshnessPolicy]
properties:
displayName: { type: string, minLength: 1, maxLength: 200 }
primaryLocale: { type: string }
timeZone: { type: string }
ingestionProposals: { type: object, required: [enabled, maximumSourceCount, maximumSourceBytes, failedRetryLimit], properties: { enabled: { type: boolean }, maximumSourceCount: { type: integer, minimum: 1, maximum: 20 }, maximumSourceBytes: { type: integer, format: int64 }, failedRetryLimit: { type: integer, minimum: 1, maximum: 3 } } }
backupPolicyExpectation: { type: object, required: [expectedIntervalHours, scheduleDescription, dailyRetentionCount, weeklyRetentionCount, monthlyRetentionCount, protectedBackupCount, restoreVerificationMaxAgeDays], properties: { expectedIntervalHours: { type: integer }, scheduleDescription: { type: string }, dailyRetentionCount: { type: integer }, weeklyRetentionCount: { type: integer }, monthlyRetentionCount: { type: integer }, protectedBackupCount: { type: integer }, restoreVerificationMaxAgeDays: { type: integer } } }
inventoryFreshnessPolicy: { type: object, required: [warningAfterHours], properties: { warningAfterHours: { type: integer } } }
PlatformSettings:
allOf:
- $ref: "#/components/schemas/PlatformSettingsRequest"
- type: object
required: [schemaVersion, eTag, isDefault, updatedAt]
properties:
schemaVersion: { type: integer, enum: [1] }
eTag: { type: string }
isDefault: { type: boolean }
updatedAt: { type: [string, "null"], format: date-time }
backupPolicyExpectation: { type: object, description: Includes enforcement External; configured schedule is an expectation, not a scheduler run. }
DataSourceRegistration:
type: object
required: [sourceKey, displayName, kind, expectedIntervalHours]
properties:
sourceKey: { type: string, maxLength: 200 }
displayName: { type: string, maxLength: 120 }
kind: { type: string, enum: [Inventory, Gitea, KnowledgeClient, ManualIntake, LeafWiki, CuratedPackage] }
expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 }
DataSourceConfiguration:
type: object
required: [displayName, enabled, expectedIntervalHours]
properties:
displayName: { type: string, maxLength: 120 }
enabled: { type: boolean }
expectedIntervalHours: { type: integer, minimum: 1, maximum: 8760 }
DataSourceObservation:
type: object
required: [success]
properties:
success: { type: boolean }
recordCount: { type: [integer, "null"], format: int64 }
errorCode: { type: [string, "null"] }
correlationId: { type: [string, "null"], maxLength: 100 }
DataSource:
type: object
required: [id, sourceKey, displayName, kind, enabled, state, configurationState, syncAuthority, expectedIntervalHours, eTag, actions]
properties:
id: { type: string, format: uuid }
sourceKey: { type: string }
displayName: { type: string }
kind: { type: string }
enabled: { type: boolean }
state: { type: string, enum: [Unknown, Current, Stale, Failed, Disabled] }
configurationState: { type: string }
syncAuthority: { type: string }
expectedIntervalHours: { type: integer }
lastAttemptAt: { type: [string, "null"], format: date-time }
lastSuccessAt: { type: [string, "null"], format: date-time }
lastFailureAt: { type: [string, "null"], format: date-time }
dataObservedAt: { type: [string, "null"], format: date-time }
recordCount: { type: [integer, "null"], format: int64 }
lastErrorCode: { type: [string, "null"] }
lastCorrelationId: { type: [string, "null"] }
eTag: { type: string }
actions: { type: array, items: { type: string } }
DataSourceList:
type: object
required: [items, nextCursor]
properties:
items: { type: array, items: { $ref: "#/components/schemas/DataSource" } }
nextCursor: { type: [string, "null"] }
BackupProtectionRequest:
type: object
required: [protected]
properties:
protected: { type: boolean }
BackupEvidence:
type: object
required: [protected, protectionVersion]
properties:
protected: { type: boolean }
protectionVersion: { type: integer }
integrityVerifiedAt: { type: [string, "null"], format: date-time }
restoreVerifiedAt: { type: [string, "null"], format: date-time }
lastRestoreSucceeded: { type: [boolean, "null"] }
BackupStatus:
type: object
required: [state, scheduleAuthority, scheduleDescription, restoreConfidence, storedBackupBytes, protectedCount, expectedIntervalHours, applicationRetentionCount, dailyRetentionExpectation, weeklyRetentionExpectation, monthlyRetentionExpectation, restoreVerificationMaxAgeDays]
properties:
state: { type: string, enum: [Unknown, Current, Stale] }
scheduleAuthority: { type: string, enum: [External] }
scheduleDescription: { type: string }
nextScheduledRun: { type: [string, "null"], format: date-time }
nextExpectedBy: { type: [string, "null"], format: date-time }
lastSuccessfulBackupAt: { type: [string, "null"], format: date-time }
lastIntegrityVerifiedAt: { type: [string, "null"], format: date-time }
lastVerifiedRestoreAt: { type: [string, "null"], format: date-time }
restoreConfidence: { type: string, enum: [Unknown, Stale, Verified] }
storedBackupBytes: { type: integer, format: int64, description: Sum of stored backup manifest sizes; not filesystem capacity. }
protectedCount: { type: integer }
expectedIntervalHours: { type: integer }
applicationRetentionCount: { type: integer }
dailyRetentionExpectation: { type: integer }
weeklyRetentionExpectation: { type: integer }
monthlyRetentionExpectation: { type: integer }
restoreVerificationMaxAgeDays: { type: integer }
AdminAuditList:
type: object
required: [items, nextCursor]
properties:
items: { type: array, items: { $ref: "#/components/schemas/AdminAuditItem" } }
nextCursor: { type: [integer, "null"], format: int64 }
AdminAuditItem:
type: object
required: [id, actorKind, source, action, summary, occurredAt]
properties:
id: { type: integer, format: int64 }
actorKind: { type: string }
actorId: { type: [string, "null"], format: uuid }
source: { type: string }
action: { type: string }
targetType: { type: [string, "null"] }
targetId: { type: [string, "null"] }
summary: { type: string }
occurredAt: { type: string, format: date-time }
correlationId: { type: [string, "null"] }
QueueCourseTaggingJobRequest:
type: object
required: [rootPageId, tag]
properties:
rootPageId: { type: string, format: uuid }
tag: { type: string }
QueueBackupJobRequest: { type: object, description: Empty object; the job snapshots the current server-side backup policy. }
BackgroundJob:
type: object
required: [id, workspaceId, actorId, type, status, createdAt, cancelRequested, progress, total, processed, succeeded, skipped, failed, errors, version]
properties:
id: { type: string, format: uuid }
workspaceId: { type: string, format: uuid }
actorId: { type: string, format: uuid }
type: { type: string }
status: { type: string, enum: [queued, running, succeeded, partially_succeeded, failed, cancelled] }
createdAt: { type: string, format: date-time }
startedAt: { type: [string, "null"], format: date-time }
completedAt: { type: [string, "null"], format: date-time }
cancelRequestedAt: { type: [string, "null"], format: date-time }
cancelRequested: { type: boolean }
progress: { type: integer, minimum: 0, maximum: 100 }
total: { type: integer }
processed: { type: integer }
succeeded: { type: integer }
skipped: { type: integer }
failed: { type: integer }
errors: { type: array, items: { type: object, properties: { code: { type: string }, message: { type: string }, itemId: { type: [string, "null"], format: uuid } } } }
result: { type: [object, "null"] }
correlationId: { type: [string, "null"] }
version: { type: integer, format: int64 }
BackgroundJobList:
type: object
required: [items]
properties:
items: { type: array, items: { $ref: "#/components/schemas/BackgroundJob" } }
CreateIngestionProposalRequest:
type: object
required: [manifest]
@@ -5243,6 +5661,41 @@ components:
$ref: "#/components/schemas/SearchResultItem"
nextCursor:
type: [string, "null"]
UnifiedSearchResponse:
type: object
required: [items, counts, nextCursor]
properties:
items:
type: array
items:
$ref: "#/components/schemas/UnifiedSearchResultItem"
counts:
$ref: "#/components/schemas/UnifiedSearchCounts"
nextCursor:
type: [string, "null"]
UnifiedSearchCounts:
type: object
required: [knowledge, projects, services, runbooks, decisions, total]
properties:
knowledge: { type: integer, minimum: 0 }
projects: { type: integer, minimum: 0 }
services: { type: integer, minimum: 0 }
runbooks: { type: integer, minimum: 0 }
decisions: { type: integer, minimum: 0 }
total: { type: integer, minimum: 0 }
UnifiedSearchResultItem:
type: object
required: [kind, id, title, summary, snippet, updatedAt, href]
properties:
kind:
type: string
enum: [knowledge, projects, services, runbooks, decisions]
id: { type: string, format: uuid }
title: { type: string }
summary: { type: string }
snippet: { type: string }
updatedAt: { type: string, format: date-time }
href: { type: string, description: Same-origin relative route to the result. }
RagPublicationManifestItem:
type: object
description: >-
+43
View File
@@ -0,0 +1,43 @@
# Durable background jobs
Course tagging and manual backup creation run as PostgreSQL-backed jobs. The admin
workspace shows persisted progress and supports cancellation. SSE sends the same
safe snapshot as the polling endpoint and closes on a terminal state; the browser
falls back to polling after an interrupted stream.
Only an enabled workspace administrator can create or execute a job. Mutation
endpoints require cookie authentication and CSRF validation. The worker rechecks
the actor's membership before work and before each course page. An idempotency key
is scoped to the workspace and actor; reusing it with a different payload returns
409. Retrying the same payload returns the existing job, including at capacity.
The job row and its `job.queued` audit event commit together. Cancellation requests
and their `job.cancel_requested` audit event also commit together; repeating a
running-job cancellation returns the current request without adding another audit
event.
The database serializes admission per workspace. Defaults under `KnowledgeHub:Jobs`
are `MaxPendingPerWorkspace=10`, `MaxPendingPerActor=3`, and
`MaxCreatedPerDay=100` (a rolling 24-hour window). Capacity rejection returns 429
before inserting a job or page snapshot. HTTP creation also has a shared budget
of ten requests per effective remote IP per minute. Only configured trusted
proxies can affect that address. These limits bound distinct-key floods across
application restarts and replicas as well as ordinary bursts.
Course tagging snapshots at most `MaxCoursePages=100000` page IDs in PostgreSQL.
The worker updates one current page at a time with its ETag, preserves content,
skips existing tags, and records safe per-page failures. A lease is renewed while
work is active; expired leases can be reclaimed with bounded retries. Default
worker concurrency is two. Cancellation stops future items and preserves the
already completed outcomes.
Terminal jobs retain their counters, bounded errors, result, actor and audit trail.
Their page-ID recovery snapshots are removed in the same transaction as the final
state. This limits large item-table growth without discarding the job history.
The daily admission budget bounds growth of that compact history. Source data is
never removed by this maintenance.
The job `Version` is a monotonic optimistic-concurrency token for persisted worker
updates and SSE event IDs. If a cancellation update wins a race with a tracked worker
save, the worker retries against the current version while keeping the cancellation
request. SSE always emits a terminal event and closes when it observes a terminal
job, including when that snapshot has the same version as the last progress event.
@@ -65,20 +65,46 @@ af.
### Stap 2 — herstellen
Maak eerst een **beschrijfbare herstelkopie** van de gekozen back-up; herstel neemt een
exclusieve opslaglease en schrijft achteraf een herstelbewijs naast het manifest. Geef
de CLI de doelconnectie via een door de operator beheerd geheimenbestand, niet via een
argument met het wachtwoord erin. Op Unix moet dat bestand alleen voor de restoregebruiker
leesbaar zijn (`chmod 600` of `chmod 400`); op Windows beperk je de ACL tot de restore-
operator/serviceaccount. Het bestand bevat één PostgreSQL-connectiereeks. De
integriteitssleutel moet eveneens beschikbaar zijn in de geconfigureerde Keys-map.
Haal de exacte `backup-integrity.key` uit beschermde externe escrow; herstel vertrouwt
geen sleutel die alleen in de back-upkopie zit. De restore verifieert de manifest-
handtekening en kopieert de vermelde bestanden eerst naar een privé-stagingmap binnen
het lege doelbijlagenvolume. Reserveer daar vrije ruimte voor ten minste de volledige
back-upinhoud. De restore gebruikt uitsluitend die geverifieerde snapshot.
```bash
docker run --rm \
--network itworx-knowledge-hub_knowledge-hub-internal \
-v knowledge-hub-backups:/var/lib/knowledge-hub/backups:ro \
-v <beschrijfbare-herstelkopie>:/var/lib/knowledge-hub/backups:rw \
-e KnowledgeHub__Paths__Backups=/var/lib/knowledge-hub/backups \
-v <map-met-backup-integriteitssleutel>:/var/lib/knowledge-hub/keys:ro \
-e KnowledgeHub__Paths__Keys=/var/lib/knowledge-hub/keys \
-v <beveiligd-doelconnectiebestand>:/run/secrets/restore-target-connection:ro \
-v <doel-bijlagen-volume>:/var/lib/knowledge-hub/attachments \
itworx/knowledge-hub:local \
dotnet ItWorx.KnowledgeHub.Api.dll \
--restore \
--backup-id=<back-up-id> \
--target-connection="Host=<doel-db-host>;Port=5432;Database=<doel-db>;Username=<gebruiker>;Password=<wachtwoord>" \
--target-connection-file=/run/secrets/restore-target-connection \
--target-attachments=/var/lib/knowledge-hub/attachments
```
De echte herstel-drill haalt de back-up via SSH op, maar leest de integriteitssleutel
uit een lokaal door de operator beheerd escrowbestand. Geef alleen het bestandspad mee;
de drill maakt tijdens uitvoering een tijdelijke kopie met modus `0600` en verwijdert
die bij opruimen:
```bash
KNOWLEDGE_HUB_BACKUP_INTEGRITY_KEY_FILE=/secure/escrow/backup-integrity.key \
bash scripts/v2-r4-real-backup-restore-drill.sh <back-up-id>
```
De doeldatabase moet **leeg** zijn — herstel weigert expliciet als er al rijen in
staan, om nooit per ongeluk bestaande gegevens te vermengen met herstelde gegevens.
@@ -86,7 +112,8 @@ Het proces rapporteert per tabel het aantal herstelde versus verwachte rijen, he
aantal herstelde bijlagen, en sluit af met `GESLAAGD` of `MISLUKT`
(afsluitcode 0 respectievelijk 1). Een `MISLUKT`-resultaat laat expliciet zien welke
tabel niet overeenkomt — er wordt nooit stilzwijgend een onvolledig herstel als
geslaagd gerapporteerd.
geslaagd gerapporteerd. Providerfoutdetails worden niet afgedrukt omdat die
connectiegegevens kunnen bevatten.
## Opstartgedrag en migraties
+16
View File
@@ -0,0 +1,16 @@
# Operationele metrieken
De applicatie exporteert alleen naar OTLP als `OTEL_EXPORTER_OTLP_ENDPOINT` expliciet is gezet. De optionele Compose-overlay `deploy/observability/compose.yaml` schakelt een Collector, Prometheus en Grafana in op een intern Docker-netwerk. Grafana bindt uitsluitend aan de loopback-interface van de host; kies een privaat beheerderswachtwoord via `KNOWLEDGE_HUB_GRAFANA_PASSWORD` buiten Git. Start met beide Compose-bestanden in hetzelfde project nadat de configuratie en opslaglocaties zijn gecontroleerd.
De Collector neemt OTLP/HTTP op poort 4318 aan en biedt de metrieken aan Prometheus op 8889. Grafana leest Prometheus als geprovisioneerde gegevensbron. De applicatie heeft vaste labels voor operatie, uitkomst, jobstatus en bewijstype. SQL, paginatitels, actoren, zoektermen, tokens en bron-URL's zijn geen metrieklabels.
De overlay gebruikt vastgelegde image-digests. De collector-default is de
beschikbare amd64-build van 0.162.0 voor de Unraid-host. Kies op een andere
architectuur expliciet een geverifieerde image via `KNOWLEDGE_HUB_COLLECTOR_IMAGE`.
De ongekwalificeerde tag `0.162.0` was tijdens de controle niet beschikbaar.
Gebruik in Prometheus onder meer `platform_operations_total`, `platform_operation_duration_seconds`, `platform_jobs`, `platform_queue_age_seconds` en `platform_evidence_age_seconds`. Een ontbrekende meting betekent onbekend: na een databasefout worden oudere snapshots uit de gauges verwijderd. De ouderdom van backups telt pas als het nieuwste opgeslagen manifest een geldig integriteitsbewijs heeft. De `inventory`-ouderdom is gebaseerd op de capturetijd van de nieuwste snapshot, niet op de latere importtijd. De applicatie toont het beleid voor externe backups als verwachting, zonder een planner-run te verzinnen.
Controleer na start een echte taak of zoekactie, vraag dan binnen een export- en scrape-interval `platform_operations_total` op via de Prometheus API en controleer de tijdreeks ook in Grafana. Een lege tijdreeks is geen bewijs van een gezonde pipeline. De geïsoleerde geautomatiseerde OTLP-test verstuurt een echte protobuf-aanvraag naar een lokale receiver en controleert dat private labels ontbreken; de volledige Collector → Prometheus → Grafana-pipeline moet in de beoogde omgeving apart worden geverifieerd.
Bronnen voor de gebruikte exporterconfiguratie: [OpenTelemetry Collector Prometheus exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/prometheusexporter/README.md) en [OTLP exporter configuratie](https://opentelemetry.io/docs/languages/sdk-configuration/otlp-exporter/).
+60
View File
@@ -0,0 +1,60 @@
# Voorkeuren en platformbeheer
## Persoonlijke instellingen
Op `/settings` kies je thema, datadichtheid, panelen, landingspagina en de volgorde
van navigatie en dashboardwidgets. Aangemelde gebruikers bewaren deze keuzes bij
hun account. Het scherm toont laden, opslaan, gesynchroniseerd, offline of een
versieconflict. Bij een conflict kun je de servervoorkeuren laden of je lokale
keuze opnieuw aanbieden. Gebruik de verplaatsknoppen met toetsenbord of touch;
slepen is een extra mogelijkheid.
## Beheerwerkruimten
`/admin` bevat het overzicht. De afzonderlijke onderdelen behandelen gebruikers,
toegang, integraties, inventaris, kennis, classificaties, achtergrondtaken,
back-ups, audit, platforminstellingen en systeeminformatie. Alleen bevoegde
beheerders hebben toegang. Een verborgen navigatielink vervangt geen API-controle.
Op `/admin/platform` bewaar je weergavenaam, locale, tijdzone, intakebeleid,
inventarisouderdom en verwachtingen voor externe backups. Verouderde formulieren
kunnen geen nieuwere serverversie overschrijven. Laad bij een conflict de actuele
waarden opnieuw voordat je wijzigingen opnieuw toepast.
De backupplanning en dagelijkse, wekelijkse en maandelijkse bewaaraantallen zijn
verwachtingen over operatorbeheer. Dit scherm start geen externe planner en voert
geen externe retentie uit. Handmatige backups verschijnen als duurzame taken op
`/admin/jobs`; de lokale backupdienst hanteert daarnaast haar geconfigureerde
retentie en respecteert beschermde backups. Zie [backup en herstel](18-backup-restore.md).
## Externe bronnen
Op `/admin/integrations` registreer je een unieke broncode, type, naam en verwacht
synchronisatie-interval. Inventarisbronnen worden automatisch geregistreerd bij
een geslaagde synchronisatie. Configuratie en waarnemingen zijn afzonderlijk:
een ingeschakelde bron is nog geen bewijs dat de laatste run geslaagd is.
De broncode `inventory:default` is gereserveerd voor de inventarisimporter en kan
niet handmatig worden geregistreerd. Uitschakelen van deze bron blokkeert volgende
imports. De importer controleert en vergrendelt de bron binnen dezelfde transactie
als de inventariswijziging: een eerder bevestigde uitschakeling wordt ook door een
reeds gestarte aanvraag gerespecteerd.
Een operator kan een geslaagde of mislukte externe run rapporteren met een
bekende recordtelling en correlatie-ID. Een onbekend aantal blijft leeg. Zo'n
rapportage is geen uitgevoerde verbindingstest. De hub voert externe synchronisatie
niet zelf uit en bewaart hier geen bronwachtwoorden. De interface toont de laatste
poging, laatste geslaagde poging en het tijdstip waarop gegevens zijn waargenomen.
## Taakvoortgang en operationeel bewijs
Een cursustag- of back-uptaak blijft op de server bestaan als je het tabblad sluit.
De detail-URL bevat het taak-ID. Annuleren stopt toekomstig werk zodra dat veilig
kan; reeds afgeronde pagina-updates blijven bewaard. Fouten en aantallen komen
uit opgeslagen serverstatus. Zie [achtergrondtaken](background-jobs.md).
Systeem- en deploymentinformatie tonen uitsluitend gemelde versie- en
runtimegegevens. Ontbrekend bewijs blijft onbekend. Gebruik het broncommit, de
image-identiteit en de toegepaste migraties samen wanneer je een release of
herstel beoordeelt. Een gezonde applicatiecontrole bewijst op zichzelf niet dat
de productieversie gelijk is aan de nieuwste repositoryversie.