@kici-dev/orchestrator 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/__test-helpers__/mock-db.d.ts +4 -0
  2. package/dist/agent/agent-version.d.ts +34 -0
  3. package/dist/agent/dispatcher.d.ts +70 -0
  4. package/dist/app.d.ts +23 -1
  5. package/dist/cache/global-eval-round-cache.d.ts +88 -0
  6. package/dist/cache/index.d.ts +3 -0
  7. package/dist/cache/pending-global-evals.d.ts +42 -0
  8. package/dist/cache/pending-inits.d.ts +10 -0
  9. package/dist/cli/commands/cluster-settings.d.ts +41 -3
  10. package/dist/cli/commands/runs.d.ts +1 -0
  11. package/dist/cli.js +1205 -620
  12. package/dist/cluster/cluster-settings-reader.d.ts +53 -1
  13. package/dist/config.d.ts +27 -0
  14. package/dist/content-requirements-cache.d.ts +55 -0
  15. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  16. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  17. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  18. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  19. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  20. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  21. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  22. package/dist/db/types.d.ts +60 -2
  23. package/dist/metrics/agent-metrics-aggregator.d.ts +2 -2
  24. package/dist/metrics/prometheus.d.ts +59 -0
  25. package/dist/orchestrator-core.d.ts +12 -1
  26. package/dist/pipeline/content-filter.d.ts +71 -0
  27. package/dist/pipeline/dispatch-matched-workflow.d.ts +247 -8
  28. package/dist/pipeline/global-eval-round.d.ts +293 -0
  29. package/dist/pipeline/job-contexts.d.ts +16 -17
  30. package/dist/pipeline/process-webhook.d.ts +7 -0
  31. package/dist/pipeline/processor.d.ts +56 -2
  32. package/dist/pipeline/route-or-dispatch-jobs.d.ts +6 -0
  33. package/dist/pipeline/test-pipeline.d.ts +12 -0
  34. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  35. package/dist/provenance/backfill-run.d.ts +10 -1
  36. package/dist/provider-registry.d.ts +38 -3
  37. package/dist/providers/github/check-status-poster.d.ts +22 -3
  38. package/dist/providers/github/commit-message.d.ts +20 -0
  39. package/dist/providers/github/file-contents.d.ts +40 -0
  40. package/dist/providers/github/index.d.ts +2 -0
  41. package/dist/providers/universal-git/config.d.ts +2 -0
  42. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  43. package/dist/queue/cleanup.d.ts +7 -1
  44. package/dist/queue/job-queue.d.ts +69 -6
  45. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  46. package/dist/registration/registration-run-match.d.ts +47 -0
  47. package/dist/reporting/check-run-reporter.d.ts +52 -1
  48. package/dist/reporting/execution-tracker.d.ts +117 -7
  49. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  50. package/dist/routes/admin-org-settings.d.ts +5 -0
  51. package/dist/routes/admin.d.ts +6 -0
  52. package/dist/scaler/manager.d.ts +10 -8
  53. package/dist/security/global-workflow-policy.d.ts +52 -12
  54. package/dist/server.js +27553 -23779
  55. package/dist/standalone.js +5840 -2207
  56. package/dist/webhook/ingest-accept.d.ts +70 -0
  57. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  58. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  59. package/dist/ws/agent-handler.d.ts +3 -0
  60. package/dist/ws/dashboard-global-workflows-handler.d.ts +30 -9
  61. package/dist/ws/execution-status-frame.d.ts +32 -0
  62. package/dist/ws/platform-client.d.ts +14 -0
  63. package/dist/ws/test-relay-handlers.d.ts +35 -10
  64. package/installer-image-digests.json +3 -3
  65. package/package.json +4 -4
  66. package/sbom.spdx.json +57 -52
  67. package/dist/pipeline/inline-eval.d.ts +0 -44
@@ -0,0 +1,70 @@
1
+ import type { WebhookInfo } from './handler.js';
2
+ import { WebhookIngestOutcome } from '../pipeline/process-webhook.js';
3
+ /**
4
+ * Seams the accept path needs. Each is a narrow function rather than the
5
+ * concrete collaborator so the sequencing below can be driven in a unit test
6
+ * without a database, an admission controller, or a pipeline.
7
+ */
8
+ export interface IngestAcceptDeps {
9
+ /**
10
+ * Reserve an admission slot. Resolves to a release function when admitted, to
11
+ * `null` when the controller shed, or to `undefined` when no controller is
12
+ * wired (tests / minimal wirings — no gate).
13
+ */
14
+ admit: () => Promise<(() => void) | null | undefined>;
15
+ /**
16
+ * Advisory duplicate probe. Non-claiming: the atomic claim stays inside the
17
+ * pipeline, where it has always been. See {@link acceptWebhookDelivery}.
18
+ */
19
+ isKnownDelivery: (deliveryId: string) => Promise<boolean>;
20
+ /** Durably store the delivery; resolves to the row id, or null at the cap. */
21
+ enqueue: (info: WebhookInfo) => Promise<number | null>;
22
+ /** Take the row's claim (`buffered` → `replaying`). False when someone else has it. */
23
+ claimRow: (rowId: number) => Promise<boolean>;
24
+ /** Mark the row done so the drain pass sweeps it. */
25
+ markProcessed: (rowId: number) => Promise<void>;
26
+ /** Hand the claim back on failure so the drain pass retries the delivery. */
27
+ releaseClaim: (rowId: number, reason: string) => Promise<boolean>;
28
+ /** Run the match-and-dispatch pipeline. */
29
+ runPipeline: (info: WebhookInfo) => Promise<WebhookIngestOutcome>;
30
+ /**
31
+ * Schedule the post-acknowledgement work. Defaults to a detached microtask;
32
+ * a test passes a collector so it can await the work deterministically.
33
+ */
34
+ schedule?: (work: () => Promise<void>) => void;
35
+ }
36
+ /**
37
+ * Accept an inbound delivery and run its pipeline afterwards.
38
+ *
39
+ * The acknowledgement a caller receives means **the delivery is durably
40
+ * queued**, not that it was matched or dispatched. That is the whole point: a
41
+ * provider's delivery attempt times out in seconds (GitHub's is 10), while one
42
+ * matched workflow's build phase alone may legitimately take ten minutes, so a
43
+ * response that waited for the pipeline turned a slow build into a failed
44
+ * delivery for work that usually succeeded.
45
+ *
46
+ * Ordering is load-bearing and each step earns its place:
47
+ *
48
+ * 1. **Admit.** A shed delivery never reaches the queue, so admission stays the
49
+ * first gate and a saturated orchestrator still answers 429 immediately.
50
+ * 2. **Probe for a duplicate.** Advisory only — a redelivery the orchestrator
51
+ * already knows about is reported as one without a row being written. The
52
+ * *authoritative* claim is still the pipeline's atomic
53
+ * `INSERT … ON CONFLICT`, so a genuine cross-instance race is arbitrated
54
+ * exactly where it always was; the loser simply learns after acknowledging.
55
+ * 3. **Enqueue durably.** Nothing is acknowledged before this row exists. At the
56
+ * row cap the delivery is shed rather than acknowledged — answering 202 for
57
+ * a delivery we did not store would turn a hang into silent data loss, which
58
+ * is strictly worse than the hang this change removes.
59
+ * 4. **Claim the row**, then acknowledge, then run the pipeline detached. The
60
+ * claim is what stops the drain pass re-injecting a delivery already in
61
+ * flight; releasing it on failure is what makes the row a retry rather than
62
+ * a tombstone. The admission slot is held across the pipeline, so in-flight
63
+ * pipeline concurrency stays bounded exactly as it was when the response
64
+ * waited for it.
65
+ *
66
+ * Returns `queued` on the acknowledge path, or `duplicate` / `shed` for the two
67
+ * pre-queue exits.
68
+ */
69
+ export declare function acceptWebhookDelivery(info: WebhookInfo, deps: IngestAcceptDeps): Promise<WebhookIngestOutcome>;
70
+ //# sourceMappingURL=ingest-accept.d.ts.map
@@ -7,10 +7,22 @@ export interface IngestOverflowBufferDeps {
7
7
  maxRows: number;
8
8
  }
9
9
  /**
10
- * Capture side of the durable overflow buffer. On a shed, the ingest path calls
11
- * {@link IngestOverflowBuffer.capture} additively (the 429 still stands). At the
12
- * row cap, capture drops the delivery from the buffer (never unbounded rows) —
13
- * the lossy fallback a retrying sender still covers.
10
+ * Capture side of the durable ingest queue. Two feeders write rows here.
11
+ *
12
+ * The **accept** path (HTTP direct ingress) enqueues every admitted delivery
13
+ * before the route answers 202: the row is what makes "durably queued" true, so
14
+ * a worker that dies mid-pipeline leaves recoverable work behind rather than a
15
+ * delivery the sender believes was accepted.
16
+ *
17
+ * The **shed** path enqueues additively when the admission controller rejects a
18
+ * delivery (the 429 still stands), so capacity recovering is enough to get the
19
+ * delivery processed without the sender redelivering.
20
+ *
21
+ * At the row cap, an enqueue fails rather than growing the table without bound.
22
+ * The two feeders differ in what that means: a shed delivery is dropped (the
23
+ * lossy fallback a retrying sender still covers), while the accept path must
24
+ * NOT answer 202 for a delivery it did not store — it sheds instead, so the
25
+ * sender learns the delivery was refused.
14
26
  */
15
27
  export declare class IngestOverflowBuffer {
16
28
  private readonly db;
@@ -18,10 +30,29 @@ export declare class IngestOverflowBuffer {
18
30
  constructor(deps: IngestOverflowBufferDeps);
19
31
  /** Current buffered-row depth. */
20
32
  currentDepth(): Promise<number>;
33
+ /**
34
+ * Persist a delivery as `buffered` and return its row id, or null when the
35
+ * buffer is at cap. Callers that must not acknowledge an unstored delivery
36
+ * check for null; the shed path treats null as a drop.
37
+ */
38
+ enqueue(d: OverflowDelivery): Promise<number | null>;
21
39
  /**
22
40
  * Persist a shed delivery. Returns true when a `buffered` row was inserted,
23
41
  * false when the buffer is at cap (delivery dropped, `cap_full` metric bumped).
24
42
  */
25
43
  capture(d: OverflowDelivery): Promise<boolean>;
44
+ /**
45
+ * Claim a specific row for processing: `buffered` → `replaying`, stamping the
46
+ * claim clock. Returns false when someone else already owns it — a drain pass
47
+ * on another instance, or a reclaim that ran while this caller was scheduling.
48
+ * The conditional update is the arbiter, so two workers can never both own a
49
+ * row and dispatch its delivery twice.
50
+ */
51
+ claimRow(id: number): Promise<boolean>;
52
+ /**
53
+ * Mark a claimed row done. The row is swept by the drain pass rather than
54
+ * deleted here, so the sweep stays the single deletion site.
55
+ */
56
+ markProcessed(id: number): Promise<void>;
26
57
  }
27
58
  //# sourceMappingURL=ingest-overflow-buffer.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import type { Kysely } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
+ import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
3
4
  import { WebhookIngestOutcome } from '../pipeline/process-webhook.js';
4
5
  import { type OverflowDelivery } from './ingest-overflow-types.js';
5
6
  export type ReinjectFn = (d: OverflowDelivery) => Promise<WebhookIngestOutcome>;
@@ -11,14 +12,29 @@ export interface IngestOverflowReplayerDeps {
11
12
  intervalMs: number;
12
13
  batchSize: number;
13
14
  maxAttempts: number;
15
+ /**
16
+ * How long a `replaying` claim may stand before it is reclaimed. Resolved per
17
+ * pass so a fleet-wide `cluster_settings` override takes effect without a
18
+ * restart; the number passed here is the configured cluster default.
19
+ */
20
+ claimTimeoutMs: number;
21
+ /** Fleet-wide override reader for {@link IngestOverflowReplayerDeps.claimTimeoutMs}. */
22
+ clusterSettings?: ClusterSettingsReader;
14
23
  }
15
24
  /**
16
- * Background drain for the durable overflow buffer. A pass runs only while the
17
- * admission controller is NOT shedding (replaying into a still-overloaded
18
- * orchestrator just re-sheds), claims the oldest `buffered` rows FIFO up to a
19
- * bounded batch, and re-injects each through the admission-gated ingest path.
20
- * A re-shed or error reverts the row to `buffered` (never lost); past the
21
- * max-attempts ceiling it goes `failed`. Successful rows are swept.
25
+ * Background drain for the durable ingest queue. Each pass first reclaims rows
26
+ * whose `replaying` claim went stale a worker killed mid-pipeline releases
27
+ * nothing, so without this its delivery would sit claimed forever and the
28
+ * durable row it acknowledged would never be worth anything. It then claims the
29
+ * oldest `buffered` rows FIFO up to a bounded batch and re-injects each through
30
+ * the admission-gated ingest path. A re-shed or error reverts the row to
31
+ * `buffered` (never lost); past the max-attempts ceiling it goes `failed`.
32
+ * Successful rows are swept.
33
+ *
34
+ * Re-injection runs only while the admission controller is NOT shedding
35
+ * (replaying into a still-overloaded orchestrator just re-sheds). Reclaiming is
36
+ * not gated that way: a stranded claim is stranded regardless of load, and the
37
+ * row it frees simply waits in `buffered` until capacity returns.
22
38
  */
23
39
  export declare class IngestOverflowReplayer {
24
40
  private readonly db;
@@ -26,6 +42,8 @@ export declare class IngestOverflowReplayer {
26
42
  private readonly intervalMs;
27
43
  private readonly batchSize;
28
44
  private readonly maxAttempts;
45
+ private readonly claimTimeoutMs;
46
+ private readonly clusterSettings;
29
47
  private reinjectDirect;
30
48
  private reinjectRelay;
31
49
  private timer;
@@ -38,10 +56,36 @@ export declare class IngestOverflowReplayer {
38
56
  stop(): void;
39
57
  /** One drain pass. Test-drivable. */
40
58
  runPass(): Promise<void>;
59
+ /**
60
+ * Revert `replaying` rows whose claim went stale to `buffered`, counting the
61
+ * abandoned attempt so a row that keeps stranding eventually goes `failed`
62
+ * instead of looping forever.
63
+ *
64
+ * A row claimed before the claim clock shipped has a null `claimed_at`; it is
65
+ * aged off `captured_at` instead, which is a safe over-estimate of how long
66
+ * it has been claimed.
67
+ */
68
+ private reclaimStaleClaims;
69
+ /**
70
+ * Release a claim: revert the row to `buffered` so the drain retries it, or
71
+ * mark it `failed` once the attempt ceiling is hit. Returns false when the
72
+ * row is gone or is no longer `replaying` — another worker owns it now, so
73
+ * this caller must not touch it.
74
+ *
75
+ * Public because the accept path's background worker holds a claim it did not
76
+ * take through {@link claimBatch} and must release it the same way on failure.
77
+ */
78
+ releaseClaim(id: number, reason: string): Promise<boolean>;
41
79
  /** Select the oldest buffered rows and claim each via a conditional update. */
42
80
  private claimBatch;
43
81
  private toDelivery;
44
82
  private replayOne;
83
+ /**
84
+ * Move a claimed row back to `buffered` (or to `failed` at the attempt
85
+ * ceiling). Every update is conditional on the row still being `replaying`,
86
+ * so a caller whose claim was reclaimed underneath it cannot yank a row a
87
+ * different worker now owns. Returns whether this call moved the row.
88
+ */
45
89
  private revertOrFail;
46
90
  private sweepReplayed;
47
91
  private refreshDepthGauge;
@@ -31,6 +31,7 @@ import type { DispatchCacheRefTracker } from '../cache/dispatch-cache-ref-tracke
31
31
  import type { PendingBuildTracker } from '../cache/pending-builds.js';
32
32
  import type { PendingInitTracker } from '../cache/pending-inits.js';
33
33
  import type { PendingDynamicTracker } from '../cache/pending-dynamics.js';
34
+ import type { PendingGlobalEvalTracker } from '../cache/pending-global-evals.js';
34
35
  import type { CacheStorage } from '../storage/types.js';
35
36
  import type { AgentMetricsAggregator } from '../metrics/agent-metrics-aggregator.js';
36
37
  import { type AgentApiRegistry } from './agent-api-registry.js';
@@ -267,6 +268,8 @@ export interface AgentWsHandlerDeps {
267
268
  pendingInits?: PendingInitTracker;
268
269
  /** Optional pending dynamic tracker for cleanup on agent disconnect. */
269
270
  pendingDynamics?: PendingDynamicTracker;
271
+ /** Optional pending global-eval-round tracker for cleanup on agent disconnect. */
272
+ pendingGlobalEvals?: PendingGlobalEvalTracker;
270
273
  /** Optional callback when agent sends encrypted secret outputs on job success. */
271
274
  onSecretOutputs?: (runId: string, jobId: string, secretOutputs: Record<string, {
272
275
  agentPublicKey: string;
@@ -2,10 +2,12 @@
2
2
  * Dashboard global-workflows handler for the orchestrator.
3
3
  *
4
4
  * Responds to `dashboard.global-workflows.*` WS messages from Platform by
5
- * reading or upserting the `org_settings` row keyed by `customer_id`. The
6
- * row holds the org-level global-workflow policy: master enable, plus the
7
- * three repo-pattern lists (allow / deny / elevate). Each list entry is a
8
- * `{routingKey?, pattern}` object that may optionally pin to one source.
5
+ * reading or upserting the `org_settings` row keyed by `customer_id`. The row
6
+ * holds the three per-org repo-pattern lists (allow / deny / elevate); each
7
+ * entry is a `{routingKey?, pattern}` object that may optionally pin to one
8
+ * source. The master enable switch is fleet-wide
9
+ * (`cluster_settings.global_workflows_enabled`) and read-only here — projected
10
+ * onto the response as `enabled`, never written from this handler.
9
11
  *
10
12
  * The handler binds a single `customerId` at construction time (via the
11
13
  * sources / generic_webhook_sources lookup at server startup) and updates
@@ -15,12 +17,17 @@ import { type Kysely } from 'kysely';
15
17
  import type { GlobalWorkflowsGetRequest, GlobalWorkflowsUpdateRequest, GlobalWorkflowSettings, RepoPatternEntry } from '@kici-dev/engine/protocol/dashboard-global-workflows';
16
18
  import type { Database, OrgSettings } from '../db/types.js';
17
19
  import type { AccessLogWriter } from '../audit/access-log.js';
20
+ import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
18
21
  interface DashboardGlobalWorkflowsHandlerDeps {
19
22
  /** Customer / org identifier — primary key on org_settings. */
20
23
  customerId: string;
21
24
  /** Send a response message back to Platform over the WS connection. */
22
25
  send: (msg: unknown) => void;
23
26
  db: Kysely<Database>;
27
+ /** Reads the fleet-wide master switch for the read-only `enabled` projection. */
28
+ clusterSettings: ClusterSettingsReader;
29
+ /** Applies when the cluster column is NULL — `config.globalWorkflowsEnabled`. */
30
+ globalWorkflowsEnabledDefault: boolean;
24
31
  /** Access log writer — records one row per read / mutation with actor attribution. */
25
32
  accessLog?: AccessLogWriter;
26
33
  }
@@ -53,11 +60,24 @@ export declare class DashboardGlobalWorkflowsHandler {
53
60
  private handleGet;
54
61
  private handleUpdate;
55
62
  private readRow;
63
+ /**
64
+ * The effective fleet-wide master switch, for the read-only projection the
65
+ * dashboard renders as a status badge.
66
+ *
67
+ * Uses the shared 10s-cached reader — unlike the admin route, which reads
68
+ * uncached because an operator does set-then-show. A dashboard badge that is
69
+ * up to 10s stale is fine; a second uncached read on every dashboard poll is
70
+ * not.
71
+ *
72
+ * An unreadable row resolves to the configured default here rather than
73
+ * failing closed. This value only decides what the UI displays; the actual
74
+ * gate is `GlobalWorkflowPolicy`, which does fail closed.
75
+ */
76
+ private effectiveEnabled;
56
77
  private upsertRow;
57
78
  private sendError;
58
79
  }
59
80
  interface NormalizedPatch {
60
- enabled: boolean;
61
81
  allowedRepos: RepoPatternEntry[] | null;
62
82
  deniedRepos: RepoPatternEntry[] | null;
63
83
  elevatedRepos: RepoPatternEntry[] | null;
@@ -69,10 +89,11 @@ interface NormalizedPatch {
69
89
  */
70
90
  export declare function buildPatch(existing: OrgSettings | undefined, msg: GlobalWorkflowsUpdateRequest): NormalizedPatch;
71
91
  /**
72
- * Project a row into the public settings shape. Returns a defaulted "disabled"
73
- * settings object when the row does not yet exist, so callers always get a
74
- * renderable state.
92
+ * Project a row into the public settings shape. `enabled` is the effective
93
+ * fleet-wide master switch, supplied by the caller (read-only here). When the
94
+ * row does not yet exist, the three per-org lists project as null so callers
95
+ * always get a renderable state.
75
96
  */
76
- export declare function rowToSettings(customerId: string, row: OrgSettings | undefined): GlobalWorkflowSettings;
97
+ export declare function rowToSettings(customerId: string, row: OrgSettings | undefined, enabled: boolean): GlobalWorkflowSettings;
77
98
  export {};
78
99
  //# sourceMappingURL=dashboard-global-workflows-handler.d.ts.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build the `execution.status` frame the orchestrator forwards to the Platform.
3
+ *
4
+ * Extracted from `server.ts`'s `onExecutionStatusChange` hook so the projection
5
+ * from an {@link ExecutionContext} onto the wire message is a pure function with
6
+ * its own tests. The hook itself stays a thin adapter: it supplies the message
7
+ * id and the clock and hands the result to the Platform client.
8
+ *
9
+ * Every field here is conditional on the context actually carrying it. That is
10
+ * load-bearing for `workflowRepoIdentifier` in particular: the tracker records
11
+ * it only when the repository that DEFINES the workflow differs from the
12
+ * repository the run acted on, so the frame must stay silent for a per-repository
13
+ * run or "present" stops marking a cross-repository global run downstream.
14
+ */
15
+ import type { ExecutionContext } from '../reporting/execution-tracker.js';
16
+ import type { ExecutionRunStatus, ExecutionStatus, InitFailure } from '@kici-dev/engine';
17
+ export interface ExecutionStatusFrameArgs {
18
+ messageId: string;
19
+ runId: string;
20
+ status: ExecutionRunStatus;
21
+ context: ExecutionContext;
22
+ jobCount: number;
23
+ startedAt: number;
24
+ timestamp: number;
25
+ completedAt?: number;
26
+ durationMs?: number;
27
+ failureReason?: string;
28
+ logBytes?: number;
29
+ initFailure?: InitFailure;
30
+ }
31
+ export declare function buildExecutionStatusFrame(args: ExecutionStatusFrameArgs): ExecutionStatus;
32
+ //# sourceMappingURL=execution-status-frame.d.ts.map
@@ -36,6 +36,20 @@ export type ReplaySendResult = {
36
36
  */
37
37
  export type { ProviderSource } from '../entry-helpers.js';
38
38
  import type { ProviderSource } from '../entry-helpers.js';
39
+ /**
40
+ * Hard ceiling on how long an admitted fire-and-forget relay pipeline may hold
41
+ * its admission slot before it is force-released. A pipeline running longer than
42
+ * this is treated as hung; releasing its slot (idempotently) prevents a
43
+ * permanent slot leak that would otherwise shrink capacity for every future
44
+ * webhook. Well above any legitimate ingest-pipeline duration.
45
+ *
46
+ * Exported because it is also the hard bound on anything a webhook waits for
47
+ * inline: the global eval round caps its raised wait ceiling at this value,
48
+ * since a ceiling past the point the relay force-releases the pipeline buys
49
+ * latency and no verdict. Two copies of "5 minutes" with nothing coupling them
50
+ * is exactly the pair that drifts.
51
+ */
52
+ export declare const ADMITTED_PIPELINE_LIFETIME_MS: number;
39
53
  export interface PlatformClientOptions {
40
54
  /** WebSocket URL of the Platform relay. */
41
55
  url: string;
@@ -24,15 +24,6 @@ export interface TestRelayHandlerDeps extends ProcessingDeps {
24
24
  agentRegistry: NonNullable<ProcessingDeps['agentRegistry']>;
25
25
  cacheStorage?: CacheStorage;
26
26
  logStorage?: LogStorage;
27
- /**
28
- * Log writer that owns the in-flight append tracking. The logs cursor
29
- * handler drains its pending appends for a terminal run before computing the
30
- * `done` flag, so the final (fire-and-forget) log chunk can't be lost to a
31
- * race with the run-status transition.
32
- */
33
- logWriter?: {
34
- drain(runId: string): Promise<void>;
35
- };
36
27
  accessLog?: AccessLogWriter;
37
28
  /** Canonical org id this orchestrator is bound to (for access_log attribution). */
38
29
  orgId?: string | null;
@@ -79,7 +70,41 @@ export interface TestRunStatusPayload {
79
70
  }>;
80
71
  done: boolean;
81
72
  }
82
- /** Snapshot a run's status + per-job status. `done` is true at a terminal run state. */
73
+ /**
74
+ * Snapshot a run's status + per-job status. `done` is true at a terminal run state.
75
+ *
76
+ * ## Why this does not check ownership, even though the run now records an owner
77
+ *
78
+ * `handleTestTrigger` records the initiator on `execution_runs.triggered_by`,
79
+ * so comparing `msg.actor` against it here looks like free defence in depth. It
80
+ * is not, for three reasons that are worth writing down because the idea
81
+ * recurs.
82
+ *
83
+ * **The orchestrator cannot authenticate the actor.** It is asserted by the
84
+ * Platform over the authenticated control-plane connection; nothing here can
85
+ * verify it. A Platform that is lying can name any principal and pass the
86
+ * check, so enforcing would defend only against a Platform that reports the
87
+ * actor faithfully and forgets to check ownership — not against a hostile one.
88
+ *
89
+ * **The two gates would disagree.** The Platform deliberately admits a caller
90
+ * with unrestricted repository scope alongside the creator, because such a
91
+ * caller can already read these runs through the dashboard run plane. Repo
92
+ * patterns do not exist on this tier — only the actor arrives — so an
93
+ * organization owner reading a developer's run would be allowed there and
94
+ * refused here. Closing that would mean the Platform asserting "this caller is
95
+ * unrestricted", which is the first problem again with extra steps.
96
+ *
97
+ * **The version skew runs the wrong way.** This component is customer-deployed
98
+ * and versions independently of the Platform, so an orchestrator upgraded to
99
+ * enforce would start refusing legitimate reads against a Platform that has not
100
+ * changed — a break the customer triggers and we cannot roll back. Fail-closed
101
+ * is right for a security control and wrong for a compatibility surface, and
102
+ * this is both.
103
+ *
104
+ * So the actor is recorded, not enforced. Enforcing here needs the caller's
105
+ * effective authorization scope on the wire (so one policy is evaluated rather
106
+ * than two), and an actor this tier can establish independently.
107
+ */
83
108
  export declare function handleTestRunStatus(msg: TestRelayRunStatusRequest, deps: TestRelayHandlerDeps): Promise<TestRunStatusPayload | {
84
109
  error: string;
85
110
  }>;
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.5.0",
3
3
  "images": {
4
- "kici-agent": "sha256:60f463973e2fa02c4e1d9311c9214e679269fa6ead3135b4c826789ca0278cbf",
5
- "kici-orchestrator": "sha256:75312fc8bee960384aa2c57ff72cf2fe65bcd52d222278632c79a5f14ccd3e27"
4
+ "kici-agent": "sha256:f5c3557be41a50900a5f7045ac769079e219766f01ef290d33b11c479d4dcb9c",
5
+ "kici-orchestrator": "sha256:596989e4804e6111f4263168e7fd41e0be9dc50d796c8ad0952af3e6e6d8e930"
6
6
  }
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/orchestrator",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Customer-deployable orchestrator for the KiCI CI/CD stack. Receives webhook events (direct or via Platform relay), matches triggers against the lock file, and dispatches jobs to connected agents.",
5
5
  "keywords": [
6
6
  "ci",
@@ -84,8 +84,8 @@
84
84
  "ws": "^8.21.1",
85
85
  "yaml": "^2.9.0",
86
86
  "zod": "^4.4.3",
87
- "@kici-dev/engine": "0.4.0",
88
- "@kici-dev/shared": "0.4.0"
87
+ "@kici-dev/engine": "0.5.0",
88
+ "@kici-dev/shared": "0.5.0"
89
89
  },
90
90
  "kici": {
91
91
  "metrics": {
@@ -100,7 +100,7 @@
100
100
  "@types/dockerode": "^4.0.1",
101
101
  "@types/ws": "^8.18.1",
102
102
  "kysely-ctl": "^0.21.0",
103
- "@kici-dev/agent": "0.4.0"
103
+ "@kici-dev/agent": "0.5.0"
104
104
  },
105
105
  "scripts": {
106
106
  "build": "node ../../scripts/build-service.mjs && tsgo --emitDeclarationOnly",